@portll/cobolwork 0.6.0 → 0.7.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 (140) hide show
  1. package/README.md +49 -18
  2. package/STABILITY.md +76 -0
  3. package/bin/cobolwork.mjs +30 -7
  4. package/lib/bms.mjs +21 -7
  5. package/lib/build.mjs +108 -53
  6. package/lib/capabilities.mjs +9 -10
  7. package/lib/cics-commands.mjs +501 -9
  8. package/lib/compliance.mjs +11 -0
  9. package/lib/consequence.mjs +13 -0
  10. package/lib/control-workers.mjs +161 -0
  11. package/lib/control.mjs +52 -3
  12. package/lib/dataflow.mjs +176 -125
  13. package/lib/db2/cursor.mjs +15 -0
  14. package/lib/db2/read.mjs +170 -0
  15. package/lib/db2/rules.mjs +77 -0
  16. package/lib/db2/stmt/alter.mjs +473 -0
  17. package/lib/db2/stmt/grant.mjs +125 -0
  18. package/lib/db2/stmt/index.mjs +141 -0
  19. package/lib/db2/stmt/misc.mjs +325 -0
  20. package/lib/db2/stmt/routine.mjs +564 -0
  21. package/lib/db2/stmt/storage.mjs +146 -0
  22. package/lib/db2/stmt/table.mjs +540 -0
  23. package/lib/db2/stmt/view.mjs +146 -0
  24. package/lib/diff.mjs +17 -5
  25. package/lib/evidence/cli.mjs +14 -3
  26. package/lib/evidence/record.mjs +1 -1
  27. package/lib/evidence/store.mjs +44 -31
  28. package/lib/exec-reading.mjs +51 -0
  29. package/lib/execution.mjs +3 -2
  30. package/lib/explain.mjs +2 -0
  31. package/lib/exploitability.mjs +11 -2
  32. package/lib/hlasm/asm/data.mjs +13 -1
  33. package/lib/hlasm/asm/sections.mjs +3 -1
  34. package/lib/hlasm/instr.mjs +25 -0
  35. package/lib/hlasm/macro/authorization.mjs +24 -4
  36. package/lib/hlasm/macro/datasets.mjs +30 -17
  37. package/lib/hlasm/macro/io.mjs +68 -45
  38. package/lib/hlasm/macro/le.mjs +2 -2
  39. package/lib/hlasm/macro/operator.mjs +32 -21
  40. package/lib/hlasm/macro/program.mjs +126 -84
  41. package/lib/hlasm/macro/recovery.mjs +10 -5
  42. package/lib/hlasm/macro/storage.mjs +64 -14
  43. package/lib/hlasm/macro/structured.mjs +1 -1
  44. package/lib/hlasm/model.mjs +35 -10
  45. package/lib/hlasm/mvs38.mjs +47 -0
  46. package/lib/hlasm/operands.mjs +10 -0
  47. package/lib/hlasm/optable.mjs +61 -0
  48. package/lib/hlasm/read.mjs +22 -8
  49. package/lib/hlasm.mjs +44 -7
  50. package/lib/ims/dli.mjs +37 -0
  51. package/lib/ims/macro/dbd.mjs +299 -0
  52. package/lib/ims/macro/psb.mjs +286 -0
  53. package/lib/ims/model.mjs +149 -0
  54. package/lib/ims/operands.mjs +23 -0
  55. package/lib/ims/read.mjs +37 -0
  56. package/lib/ims/rules.mjs +135 -0
  57. package/lib/ironwork.mjs +17 -13
  58. package/lib/kernel/pds-archive.mjs +256 -0
  59. package/lib/kernel/registry.mjs +14 -8
  60. package/lib/kernel/shared-pass.mjs +34 -9
  61. package/lib/kernel/source-tree.mjs +104 -36
  62. package/lib/kernel/version-key.mjs +17 -0
  63. package/lib/layout.mjs +26 -31
  64. package/lib/parser.mjs +136 -17
  65. package/lib/pli/cursor.mjs +15 -0
  66. package/lib/pli/expr.mjs +101 -0
  67. package/lib/pli/include.mjs +82 -0
  68. package/lib/pli/layout.mjs +125 -0
  69. package/lib/pli/lex.mjs +198 -0
  70. package/lib/pli/program.mjs +280 -0
  71. package/lib/pli/rules/based.mjs +95 -0
  72. package/lib/pli/rules/conditions.mjs +68 -0
  73. package/lib/pli/rules/entry.mjs +130 -0
  74. package/lib/pli/rules/index.mjs +24 -0
  75. package/lib/pli/rules/preprocessor.mjs +55 -0
  76. package/lib/pli/statements.mjs +130 -0
  77. package/lib/pli/stmt/alloc.mjs +45 -0
  78. package/lib/pli/stmt/assignment.mjs +56 -0
  79. package/lib/pli/stmt/call.mjs +104 -0
  80. package/lib/pli/stmt/conditions.mjs +94 -0
  81. package/lib/pli/stmt/control.mjs +219 -0
  82. package/lib/pli/stmt/declare.mjs +149 -0
  83. package/lib/pli/stmt/exec.mjs +55 -0
  84. package/lib/pli/stmt/io.mjs +239 -0
  85. package/lib/pli/stmt/misc.mjs +4 -0
  86. package/lib/pli/stmt/preprocessor.mjs +242 -0
  87. package/lib/pli/stmt/procedure.mjs +258 -0
  88. package/lib/pli/stmt/stream.mjs +283 -0
  89. package/lib/pli/storage.mjs +129 -0
  90. package/lib/precompile-check.mjs +124 -0
  91. package/lib/precompile-cics.mjs +7 -3
  92. package/lib/reach.mjs +11 -2
  93. package/lib/revision.json +1 -1
  94. package/lib/sarif.mjs +41 -3
  95. package/lib/scan.mjs +7 -1
  96. package/lib/sets/abend.mjs +16 -6
  97. package/lib/sets/cics.mjs +15 -35
  98. package/lib/sets/compile.mjs +41 -15
  99. package/lib/sets/crypto.mjs +5 -3
  100. package/lib/sets/ddl.mjs +36 -0
  101. package/lib/sets/flow.mjs +18 -1
  102. package/lib/sets/hidden.mjs +5 -3
  103. package/lib/sets/hlasm.mjs +72 -12
  104. package/lib/sets/ims.mjs +139 -0
  105. package/lib/sets/log.mjs +6 -6
  106. package/lib/sets/opaque.mjs +27 -7
  107. package/lib/sets/pli.mjs +40 -0
  108. package/lib/sets/recon.mjs +5 -3
  109. package/lib/sets/secrets.mjs +5 -3
  110. package/lib/sets/semantics.mjs +3 -0
  111. package/lib/sets/web.mjs +36 -22
  112. package/lib/site.mjs +10 -0
  113. package/lib/sources.mjs +80 -20
  114. package/lib/statement-cursor.mjs +67 -0
  115. package/lib/verify.mjs +3 -2
  116. package/lib/version.mjs +6 -0
  117. package/package.json +3 -2
  118. package/rules/compliance-cobit2019.json +432 -1
  119. package/rules/compliance-dora.json +414 -1
  120. package/rules/compliance-ffiec.json +414 -1
  121. package/rules/compliance-nist80053.json +466 -1
  122. package/rules/hlasm-optables.json +8024 -0
  123. package/schema/cobolwork-baseline.schema.json +36 -0
  124. package/schema/cobolwork-build-provenance.schema.json +187 -0
  125. package/schema/cobolwork-build.schema.json +382 -0
  126. package/schema/cobolwork-capabilities.schema.json +239 -0
  127. package/schema/cobolwork-diff.schema.json +217 -0
  128. package/schema/cobolwork-evidence.schema.json +161 -0
  129. package/schema/cobolwork-execution.schema.json +53 -0
  130. package/schema/cobolwork-explain.schema.json +360 -0
  131. package/schema/cobolwork-finding.schema.json +465 -0
  132. package/schema/cobolwork-flow.schema.json +465 -0
  133. package/schema/cobolwork-gate.schema.json +211 -0
  134. package/schema/cobolwork-inventory.schema.json +206 -0
  135. package/schema/cobolwork-parse.schema.json +105 -0
  136. package/schema/cobolwork-reach.schema.json +74 -0
  137. package/schema/cobolwork-report.schema.json +559 -0
  138. package/schema/cobolwork-witness.schema.json +107 -0
  139. package/schema/cobolwork.baseline.schema.json +101 -0
  140. package/schema/cobolwork.site.schema.json +116 -0
package/lib/build.mjs CHANGED
@@ -6,20 +6,21 @@ import { checkEquivalence } from './equivalence.mjs';
6
6
  import { optionChanges } from './option-diff.mjs';
7
7
  import { createHash } from 'node:crypto';
8
8
  import { spawnSync } from 'node:child_process';
9
- import { existsSync, readFileSync, statSync } from 'node:fs';
10
- import { basename, extname, join, resolve } from 'node:path';
9
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { basename, dirname, extname, join, resolve } from 'node:path';
11
12
  import { parseAllowedSigners } from './evidence/sshsig.mjs';
12
13
  import { scanAll } from './scan.mjs';
13
14
  import { withRefs, configurationChanges, WORKING_TREE } from './diff.mjs';
14
15
  import { pairLists, findExecutable } from './gate.mjs';
15
16
  import { pairingKeys } from './kernel/identity.mjs';
16
- import { directoryTree, resolvesInside } from './kernel/source-tree.mjs';
17
+ import { directoryTree, pdsExportTree, resolvesInside } from './kernel/source-tree.mjs';
17
18
  import { printable } from './kernel/printable.mjs';
18
19
  import { ALL_RULES } from './kernel/registry.mjs';
19
20
  import { loadBaseline, applyBaseline, SUPPRESSING } from './baseline.mjs';
20
21
  import { loadSite, SITE_FILE } from './site.mjs';
21
- import { isProgram, isJcl, isSource, readSource, relPath } from './sources.mjs';
22
- import { parseJclFile } from './jcl.mjs';
22
+ import { isProgram, isJcl, isSource, relPath } from './sources.mjs';
23
+ import { parseJcl } from './jcl.mjs';
23
24
  import { POLICY_FILE, treePolicy, floorPolicy, combinePolicies, canonical, policyHash } from './policy.mjs';
24
25
  import { classesOf, tierOf, TIERS, TIER_RANK } from './consequence.mjs';
25
26
  import { optionCards, enterpriseChecks, compilerChecks, scriptedChecks, compilerInvocations, compilerTasks, compileStepOptions, compilerOf, olderThan } from './options.mjs';
@@ -27,6 +28,7 @@ import { isBuildFile, pinsIn } from './sets/build.mjs';
27
28
  import { FLOW_MODEL, TOOL_VERSION } from './version.mjs';
28
29
  import { checkWithIronwork, ironworkVerdict, ironworkReasons, ironworkVersion, listed } from './ironwork.mjs';
29
30
  import { buildFileIndex } from './parser.mjs';
31
+ import { precompileCheck } from './precompile-check.mjs';
30
32
 
31
33
  export const BUILD_SCHEMA_VERSION = 1;
32
34
  export const BUILD_EXIT = { pass: 0, fail: 1, undecided: 3, compilerFailed: 4 };
@@ -87,12 +89,12 @@ const MAX_BARE_SCRIPT_BYTES = 64 << 10;
87
89
  const kindOf = (rel) => (/(^|\/)(gnu)?makefile$|\.mk$/i.test(rel) ? 'make' : /(^|\/)dockerfile$/i.test(rel) ? 'docker'
88
90
  : /\.(bat|cmd)$/i.test(rel) ? 'batch' : /\.ps1$/i.test(rel) ? 'powershell' : /\.ya?ml$/i.test(rel) ? 'yaml' : 'sh');
89
91
 
90
- function scriptInvocations(file, rel) {
91
- if (EDITOR_TASKS.test(rel)) return compilerTasks(readSource(file).text);
92
- const size = statSync(file).size;
93
- if (BUILD_SCRIPT.test(rel) && size <= MAX_SCRIPT_BYTES) return compilerInvocations(readSource(file).text, { kind: kindOf(rel) });
92
+ function scriptInvocations(tree, file, rel) {
93
+ if (EDITOR_TASKS.test(rel)) return compilerTasks(tree.text(file).text);
94
+ const size = tree.kind === 'directory' ? statSync(file).size : tree.bytes(file).length;
95
+ if (BUILD_SCRIPT.test(rel) && size <= MAX_SCRIPT_BYTES) return compilerInvocations(tree.text(file).text, { kind: kindOf(rel) });
94
96
  if (!extname(file) && size <= MAX_BARE_SCRIPT_BYTES) {
95
- const bytes = readFileSync(file);
97
+ const bytes = tree.bytes(file);
96
98
  return bytes.includes(0) ? [] : compilerInvocations(bytes.toString('latin1'), { kind: 'sh' });
97
99
  }
98
100
  return [];
@@ -100,14 +102,14 @@ function scriptInvocations(file, rel) {
100
102
 
101
103
  // How the repository says it compiles its programs: the cobc and gcobol commands its build scripts
102
104
  // run, and the Enterprise COBOL compile steps its JCL holds.
103
- function declaredCompiles(root, files) {
105
+ function declaredCompiles(tree, files) {
104
106
  const invocations = [];
105
107
  const parsedJcl = [];
106
108
  for (const f of files) {
107
- const rel = relPath(root, f);
109
+ const rel = tree.rel(f);
108
110
  try {
109
- if (isJcl(f)) parsedJcl.push({ ...parseJclFile(f), file: rel });
110
- else if (!isProgram(f)) for (const inv of scriptInvocations(f, rel)) invocations.push({ ...inv, file: rel });
111
+ if (isJcl(f)) parsedJcl.push({ ...parseJcl(tree.text(f).text, f), file: rel });
112
+ else if (!isProgram(f)) for (const inv of scriptInvocations(tree, f, rel)) invocations.push({ ...inv, file: rel });
111
113
  } catch { /* unreadable: the scan counts it, and it declares nothing here */ }
112
114
  }
113
115
  const steps = new Map();
@@ -121,13 +123,13 @@ function declaredCompiles(root, files) {
121
123
  const memberName = (file) => basename(file).replace(/\.[^.]*$/, '').toUpperCase();
122
124
 
123
125
  // The GnuCOBOL version the repository's build files pin, the oldest where they pin several.
124
- function pinnedGnucobol(root, files) {
126
+ function pinnedGnucobol(tree, files) {
125
127
  let oldest = null;
126
128
  for (const file of files.filter(isBuildFile)) {
127
129
  let text;
128
- try { text = readSource(file).text; } catch { continue; }
130
+ try { text = tree.text(file).text; } catch { continue; }
129
131
  for (const p of pinsIn(text)) {
130
- if (p.product === 'gnucobol' && (!oldest || olderThan(p.version, oldest.version))) oldest = { version: p.version, where: relPath(root, file) };
132
+ if (p.product === 'gnucobol' && (!oldest || olderThan(p.version, oldest.version))) oldest = { version: p.version, where: tree.rel(file) };
131
133
  }
132
134
  }
133
135
  return oldest;
@@ -139,22 +141,22 @@ function pinnedGnucobol(root, files) {
139
141
  // `ok` is false where a required check is not generated and null where no one can tell; `forbidden`
140
142
  // counts options the policy's forbid list names. `generated` maps each program and build script to
141
143
  // the checks it generates, for telling a change that removes one.
142
- function optionsCheck({ root, allow, site, policy, compiler }) {
144
+ function optionsCheck({ root, tree: given = null, allow, site, policy, compiler }) {
143
145
  const required = policy.checks;
144
146
  const kind = compiler && compilerOf(compiler[0]);
145
147
  if (compiler && !kind) {
146
148
  return { ok: null, forbidden: 0, reasons: [`the gate reads the options of cobc, of gcobol and of Enterprise COBOL option cards; for ${printable(compiler[0], 60)} it cannot tell what reaches the compiler`], add: [], generated: new Map() };
147
149
  }
148
- const tree = directoryTree(root);
150
+ const tree = given || directoryTree(root);
149
151
  const files = [...new Set(tree.list())].filter((p) => !allow || allow.has(p)).sort();
150
- const pinned = pinnedGnucobol(root, files);
152
+ const pinned = pinnedGnucobol(tree, files);
151
153
  if (compiler) {
152
154
  const r = compilerChecks(kind, compiler.slice(1), { required, forbid: policy.forbid[kind], pinned });
153
155
  const missing = r.checks.filter((c) => c.ok === false).map((c) => c.why);
154
156
  return { ok: missing.length || r.forbidden.length ? false : true, forbidden: r.forbidden.length, reasons: [...r.forbidden, ...missing], add: r.forbidden.length ? [] : r.add, compiler: kind, generated: new Map() };
155
157
  }
156
158
  const programs = files.filter(isProgram);
157
- const declared = declaredCompiles(root, files);
159
+ const declared = declaredCompiles(tree, files);
158
160
  let ok = true;
159
161
  let forbidden = 0;
160
162
  const reasons = [];
@@ -177,7 +179,7 @@ function optionsCheck({ root, allow, site, policy, compiler }) {
177
179
  let unknown = 0;
178
180
  for (const file of programs) {
179
181
  let text;
180
- try { text = readSource(file).text; } catch { open(); reasons.push(`${printable(relPath(root, file), 120)} could not be read, so its options are not known`); continue; }
182
+ try { text = tree.text(file).text; } catch { open(); reasons.push(`${printable(tree.rel(file), 120)} could not be read, so its options are not known`); continue; }
181
183
  const cards = optionCards(text);
182
184
  const steps = declared.steps.get(memberName(file)) || [];
183
185
  const siteOptions = site.compilerOptions || [];
@@ -186,10 +188,10 @@ function optionsCheck({ root, allow, site, policy, compiler }) {
186
188
  continue;
187
189
  }
188
190
  const r = enterpriseChecks({ required, site: siteOptions, steps, cards, forbid: policy.forbid.enterprise });
189
- record(relPath(root, file), r.checks);
191
+ record(tree.rel(file), r.checks);
190
192
  forbidden += r.forbidden.length;
191
193
  const failed = r.checks.filter((c) => c.ok === false).map((c) => c.why);
192
- for (const why of [...failed, ...r.forbidden]) fail(`${relPath(root, file)}: ${why}`);
194
+ for (const why of [...failed, ...r.forbidden]) fail(`${tree.rel(file)}: ${why}`);
193
195
  if (!failed.length && !r.forbidden.length && r.checks.some((c) => c.ok === null)) { unknown++; open(); }
194
196
  }
195
197
  if (unknown) reasons.push(`${unknown} program(s) have no option this gate can read for ${required.join(', ')}: no compile step or build script names them; declare the estate's defaults as compilerOptions in cobolwork.site.json`);
@@ -212,20 +214,41 @@ function optionsRemoved(before, after) {
212
214
  return removed;
213
215
  }
214
216
 
215
- // The files a scan of `root` reads, hashed, for the provenance record.
216
- function sourceHashes(root, allow) {
217
- const tree = directoryTree(root);
217
+ // The files a scan of `root`, or of a PDS export's tree, reads, hashed, for the provenance record.
218
+ function sourceHashes(root, allow, given = null) {
219
+ const tree = given || directoryTree(root);
218
220
  return [...new Set(tree.list())].filter((p) => isSource(p) && (!allow || allow.has(p))).sort()
219
- .map((p) => ({ path: relPath(root, p), sha256: sha256(readFileSync(p)) }))
221
+ .map((p) => ({ path: tree.rel(p), sha256: sha256(tree.bytes(p)) }))
220
222
  .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
221
223
  }
222
224
 
225
+ // A PDS export's members written out as DATA.SET/MEMBER for ironwork, which reads files: the
226
+ // programs to check, and each copy library's directory. `fn` gets the layout's root and those.
227
+ function withLayout(tree, allow, fn) {
228
+ const root = mkdtempSync(join(tmpdir(), 'cobolwork-pds-layout-'));
229
+ try {
230
+ const programs = [];
231
+ for (const p of tree.list()) {
232
+ const file = join(root, ...tree.rel(p).split('/'));
233
+ mkdirSync(dirname(file), { recursive: true });
234
+ writeFileSync(file, tree.text(p).text, 'latin1');
235
+ if (isProgram(p) && (!allow || allow.has(p))) programs.push(file);
236
+ }
237
+ return fn(root, programs.sort(), tree.index.copyDirs.map((d) => join(root, ...tree.rel(d).split('/'))));
238
+ } finally { rmSync(root, { recursive: true, force: true }); }
239
+ }
240
+
223
241
  // ironwork checks the head tree, which in ratchet mode exists only while judge runs, so it runs
224
242
  // here rather than after the verdict the way a caller's compiler does.
225
- function ironworkCompile(path, { root, allow, copylibs, env }) {
226
- const result = checkWithIronwork(path, root, { allow, copylibs, env });
243
+ function ironworkCompile(path, { root, tree = null, allow, copylibs, env }) {
244
+ if (tree) return withLayout(tree, allow, (layout, programs, copyDirs) => ironworkChecked(path, { root: layout, programs, copyDirs, copylibs, env }));
245
+ return ironworkChecked(path, { root, allow, copylibs, env });
246
+ }
247
+
248
+ function ironworkChecked(path, { root, allow = null, programs = null, copyDirs = null, copylibs, env }) {
249
+ const result = checkWithIronwork(path, root, { allow, programs, copyDirs, copylibs, env });
227
250
  const check = ironworkVerdict(result);
228
- const libraries = [...buildFileIndex(root).copyDirs.map((d) => relPath(root, d) || '.'), ...copylibs];
251
+ const libraries = [...(copyDirs || buildFileIndex(root).copyDirs).map((d) => relPath(root, d) || '.'), ...copylibs];
229
252
  return {
230
253
  check,
231
254
  reasons: ironworkReasons(result),
@@ -238,16 +261,27 @@ function ironworkCompile(path, { root, allow, copylibs, env }) {
238
261
  };
239
262
  }
240
263
 
241
- function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compiler, compilerPath, ironworkPath, env, now, advisoryFeeds, copylibs, equivalenceFiles = [], allowed = null, refs = null }) {
264
+ // The members of a PDS export whose files `allow` holds: an archive's members by the archive's file.
265
+ function membersAllowed(tree, allow) {
266
+ return new Set(tree.list().filter((p) => allow.has(resolve(tree.dir, tree.origin(p).replace(/\([^)]*\)$/, '')))));
267
+ }
268
+
269
+ function judge({ baseDir, headDir, allow: given, policyRoot, floor, noBaseline, compiler, compilerPath, ironworkPath, env, now, advisoryFeeds, copylibs, equivalenceFiles = [], allowed = null, refs = null, pdsExport = false }) {
242
270
  const repoPolicy = treePolicy(policyRoot);
243
271
  if (repoPolicy && repoPolicy.problems.length) throw refusal(`the repository policy does not validate: ${repoPolicy.problems.join('; ')}`);
244
272
  const { policy, setBy, ignored } = combinePolicies({ floor: floor ? floor.raw : null, repository: repoPolicy ? repoPolicy.raw : null });
245
273
  const ratchet = baseDir !== null;
274
+ // A PDS export is judged as its members, each side read as scan --pds-export reads it; the policy,
275
+ // site file and baseline stay files beside the members.
276
+ const trees = pdsExport ? { base: ratchet ? pdsExportTree(baseDir, { systemDirs: copylibs }) : null, head: pdsExportTree(headDir, { systemDirs: copylibs }) } : null;
277
+ const allow = trees && given ? membersAllowed(trees.head, given) : given;
246
278
  // In ratchet mode the change is judged by its base's site file, not by one it wrote itself.
247
- const scanOpts = { ...(ratchet ? { site: join(baseDir, SITE_FILE) } : {}), ...(advisoryFeeds ? { advisoryFeeds } : {}), systemDirs: copylibs };
248
- const before = ratchet ? scanAll(baseDir, scanOpts) : null;
249
- const after = scanAll(headDir, { ...scanOpts, allow });
250
- const introduced = ratchet ? introducedIn(before, after, baseDir, headDir) : new Set();
279
+ const siteFile = ratchet ? join(baseDir, SITE_FILE) : trees && existsSync(join(headDir, SITE_FILE)) ? join(headDir, SITE_FILE) : null;
280
+ const scanOpts = { ...(siteFile ? { site: siteFile } : {}), ...(advisoryFeeds ? { advisoryFeeds } : {}), systemDirs: copylibs };
281
+ const scanOf = (dir, tree, extra = {}) => (tree ? scanAll(tree.root, { ...scanOpts, ...extra, tree, feedRoot: dir }) : scanAll(dir, { ...scanOpts, ...extra }));
282
+ const before = ratchet ? scanOf(baseDir, trees?.base) : null;
283
+ const after = scanOf(headDir, trees?.head, { allow });
284
+ const introduced = ratchet ? introducedIn(before, after, trees ? trees.base.root : baseDir, trees ? trees.head.root : headDir) : new Set();
251
285
 
252
286
  const bounded = boundBaseline(noBaseline ? null : loadBaseline(policyRoot), policy.waivers.maxDays);
253
287
  applyBaseline(after, bounded.baseline, { now });
@@ -276,20 +310,22 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
276
310
  reasons.push(`coverage is incomplete${sets.length ? ` in ${sets.join(', ')}` : ''}${policy.coverage === 'block' ? ', and the policy says block, so what was not read cannot be told from what is clean' : '; the policy says warn'}`);
277
311
  }
278
312
  const site = loadSite(policyRoot);
279
- const options = optionsCheck({ root: headDir, allow, site, policy, compiler });
313
+ const options = optionsCheck({ root: headDir, tree: trees?.head, allow, site, policy, compiler });
280
314
  reasons.push(...options.reasons);
281
315
  // A missing run-time check is a weaker build, not a defect: warn passes it relaxed. What the
282
316
  // policy forbids by name, and a check the change itself removed, still fail.
283
- const removed = ratchet && !compiler ? optionsRemoved(optionsCheck({ root: baseDir, allow: null, site, policy, compiler }), options) : [];
317
+ const removed = ratchet && !compiler ? optionsRemoved(optionsCheck({ root: baseDir, tree: trees?.base, allow: null, site, policy, compiler }), options) : [];
284
318
  reasons.push(...removed.slice(0, MAX_PROGRAM_REASONS));
285
- const optionsChanged = ratchet ? programOptionChanges(baseDir, headDir, allow) : [];
319
+ const optionsChanged = ratchet ? programOptionChanges(baseDir, headDir, allow, trees) : [];
286
320
  for (const c of optionsChanged.slice(0, MAX_PROGRAM_REASONS)) reasons.push(`${c.path}:${c.line} changes ${c.option} from ${c.base} to ${c.head}, which changes what the program's statements do without changing a statement`);
287
- const equivalence = ratchet && (equivalenceFiles.length || policy.requireEquivalence !== 'never')
321
+ const wantsEquivalence = ratchet && (equivalenceFiles.length || policy.requireEquivalence !== 'never');
322
+ const equivalence = wantsEquivalence && !trees
288
323
  ? checkEquivalence({ baseDir, headDir, allow, files: equivalenceFiles, allowed, mode: policy.requireEquivalence, ...(refs || {}), systemDirs: copylibs, env })
289
- : null;
324
+ : wantsEquivalence ? { problems: [], required: null, programs: [], mode: policy.requireEquivalence, notRun: 'equivalence is not run over a PDS export' } : null;
290
325
  if (equivalence) {
291
326
  reasons.push(...equivalence.problems);
292
- if (equivalence.required === null) reasons.push('the policy requires equivalence for machine-authored changes, and whether this one is could not be read from the commits');
327
+ if (equivalence.notRun) reasons.push(`${equivalence.notRun}, and the policy or --equivalence asks for it`);
328
+ else if (equivalence.required === null) reasons.push('the policy requires equivalence for machine-authored changes, and whether this one is could not be read from the commits');
293
329
  if (equivalence.required) for (const p of equivalence.programs.filter((x) => !x.ok).slice(0, MAX_PROGRAM_REASONS)) reasons.push(`${p.path}: ${p.because}, and the policy requires equivalence${equivalence.mode === 'machineAuthored' ? ' for a machine-authored change' : ''}`);
294
330
  }
295
331
  const relaxOptions = policy.options === 'warn' && options.ok !== true && !options.forbidden && !removed.length;
@@ -307,7 +343,7 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
307
343
  const verdict = decided.includes(false) ? 'fail' : decided.includes(null) ? 'undecided' : 'pass';
308
344
  let compiled = null;
309
345
  if (ironworkPath && verdict === 'pass') {
310
- const iw = ironworkCompile(ironworkPath, { root: headDir, allow, copylibs, env });
346
+ const iw = ironworkCompile(ironworkPath, { root: headDir, tree: trees?.head, allow, copylibs, env });
311
347
  compiled = iw.compiled;
312
348
  checks.compile = iw.check;
313
349
  reasons.push(...iw.reasons);
@@ -352,23 +388,26 @@ function judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compile
352
388
  byTier,
353
389
  coverageIncomplete: incomplete,
354
390
  ...(after.summary.baseline && after.summary.baseline.problems ? { baselineProblems: after.summary.baseline.problems } : {}),
391
+ ...(trees ? { pdsExport: { ...(trees.base ? { base: trees.base.export } : {}), head: trees.head.export } } : {}),
355
392
  },
356
393
  };
357
- const hashes = sourceHashes(headDir, allow);
394
+ const hashes = sourceHashes(headDir, allow, trees?.head);
358
395
  return { doc, report: after, policy, setBy, hashes, add: options.add, compilerPath };
359
396
  }
360
397
 
361
398
  // The watched options in force changed by the change, per program present on both sides.
362
- function programOptionChanges(baseDir, headDir, allow) {
399
+ function programOptionChanges(baseDir, headDir, allow, trees = null) {
363
400
  const out = [];
364
- const files = directoryTree(headDir).list().filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
401
+ const headTree = trees ? trees.head : directoryTree(headDir);
402
+ const baseTree = trees ? trees.base : directoryTree(baseDir);
403
+ const files = headTree.list().filter((p) => isProgram(p) && (!allow || allow.has(p))).sort();
365
404
  for (const head of files) {
366
- const path = relPath(headDir, head);
367
- const base = join(baseDir, path);
368
- if (!existsSync(base)) continue;
405
+ const path = headTree.rel(head);
406
+ const base = resolve(baseTree.root, ...path.split('/'));
407
+ if (!(trees ? baseTree.contains(base) : existsSync(base))) continue;
369
408
  let before;
370
409
  let after;
371
- try { before = readSource(base).text; after = readSource(head).text; } catch { continue; }
410
+ try { before = baseTree.text(base).text; after = headTree.text(head).text; } catch { continue; }
372
411
  for (const c of optionChanges(before, after)) out.push({ path, ...c });
373
412
  }
374
413
  return out;
@@ -383,7 +422,7 @@ function policyChanged(baseDir, headDir) {
383
422
 
384
423
  // Runs the gate over `repo`. `compiler` is the argument vector after `--`, or null. Returns
385
424
  // { doc, report, provenance, exit }; throws with code EBUILD when it could not run.
386
- export function build(repo, { base = null, head = null, policy = null, noBaseline = false, compiler = null, ironwork = null, advisoryFeeds = null, copylibs = [], equivalence = [], allowed: given = null, allowedSigners = null, now = new Date().toISOString(), env = process.env } = {}) {
425
+ export function build(repo, { base = null, head = null, policy = null, noBaseline = false, compiler = null, ironwork = null, advisoryFeeds = null, copylibs = [], equivalence = [], allowed: given = null, allowedSigners = null, now = new Date().toISOString(), env = process.env, pdsExport = false, precompile = false } = {}) {
387
426
  const floor = policy ? floorPolicy(policy, repo) : null;
388
427
  if (floor && floor.problems.length) throw refusal(floor.problems.join('; '));
389
428
  let allowed = given;
@@ -393,6 +432,9 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
393
432
  allowed = parseAllowedSigners(readFileSync(path, 'utf8'));
394
433
  }
395
434
  if (compiler && ironwork) throw refusal('one compiler per build: -- names one and --ironwork another');
435
+ if (precompile && !compiler) throw refusal('--precompile translates the programs a compiler command names, so it needs -- and the compiler');
436
+ if (precompile && !compilerOf(compiler[0])) throw refusal(`--precompile checks a translation with cobc or gcobol -fsyntax-only; for ${printable(compiler[0], 60)} it cannot`);
437
+ if (pdsExport && compiler) throw refusal('a PDS export is checked with --ironwork; the compiler after -- reads the files of a repository, not the members of an export');
396
438
  let compilerPath = null;
397
439
  if (compiler) {
398
440
  if (!compiler.length) throw refusal('nothing follows --, so there is no compiler to run');
@@ -406,7 +448,7 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
406
448
  if (!found.path) throw refusal(`--ironwork: ${found.why}`);
407
449
  ironworkPath = found.path;
408
450
  }
409
- const run = (baseDir, headDir, allow, policyRoot) => judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compiler, compilerPath, ironworkPath, env, now, advisoryFeeds, copylibs, equivalenceFiles: equivalence, allowed, refs: base ? { repo, base, head: head || 'HEAD' } : null });
451
+ const run = (baseDir, headDir, allow, policyRoot) => judge({ baseDir, headDir, allow, policyRoot, floor, noBaseline, compiler, compilerPath, ironworkPath, env, now, advisoryFeeds, copylibs, equivalenceFiles: equivalence, allowed, refs: base ? { repo, base, head: head || 'HEAD' } : null, pdsExport });
410
452
  const judged = base
411
453
  ? withRefs(repo, base, head, {}, (baseDir, headDir, scoped) => {
412
454
  const r = run(baseDir, headDir, scoped.allow || null, baseDir);
@@ -424,7 +466,20 @@ export function build(repo, { base = null, head = null, policy = null, noBaselin
424
466
  if (doc.checks.compile === false) exit = BUILD_EXIT.compilerFailed;
425
467
  else if (doc.checks.compile === null && !doc.relaxed.includes('compile')) exit = BUILD_EXIT.undecided;
426
468
  }
427
- if (compiler && doc.verdict === 'pass') {
469
+ // A program the compiler refuses once its EXEC SQL and CICS are translated stops the build before
470
+ // the caller's compiler runs, as a fail stops it.
471
+ if (precompile && doc.verdict === 'pass') {
472
+ const checked = precompileCheck({ compilerPath, argv: compiler.slice(1), repo, copylibs, env });
473
+ doc.precompiled = checked;
474
+ const refused = checked.programs.filter((p) => !p.ok);
475
+ doc.checks.precompile = refused.length === 0;
476
+ if (refused.length) {
477
+ exit = BUILD_EXIT.compilerFailed;
478
+ doc.reasons.push(`${refused.length} of ${checked.programs.length} program(s) holding EXEC SQL or CICS do not compile once precompiled`);
479
+ for (const p of refused.slice(0, MAX_PROGRAM_REASONS)) doc.reasons.push(`${p.path}: ${(p.errors || [])[0] || 'the compiler exited non-zero'}`);
480
+ }
481
+ }
482
+ if (compiler && doc.verdict === 'pass' && doc.checks.precompile !== false) {
428
483
  const argv = [...compiler.slice(1), ...judged.add];
429
484
  // The compiler's own output goes to standard error: standard output carries the document.
430
485
  const r = spawnSync(compilerPath, argv, { stdio: ['ignore', 2, 2], env, windowsHide: true });
@@ -6,7 +6,7 @@ import { RULE_SETS, ALL_RULES } from './kernel/registry.mjs';
6
6
  import { EVIDENCE } from './kernel/findings.mjs';
7
7
  import { FINGERPRINT_VERSION } from './kernel/identity.mjs';
8
8
  import { PROGRAM_EXT, COPY_EXT, JCL_EXT, BMS_EXT } from './sources.mjs';
9
- import { FLOW_MODEL, SCHEMA_VERSION, TOOL_VERSION } from './version.mjs';
9
+ import { FLOW_MODEL, SCHEMA_VERSION, TOOL_VERSION, EXPLAIN_SCHEMA_VERSION, PARSE_SCHEMA_VERSION, BASELINE_RESULT_SCHEMA_VERSION, EVIDENCE_RESULT_SCHEMA_VERSION } from './version.mjs';
10
10
  import { BUILD_SCHEMA_VERSION, BUILD_EXIT } from './build.mjs';
11
11
  import { VERDICT_EXIT } from './gate.mjs';
12
12
  import { CHECKS, COMPILERS, DEFAULT_POLICY } from './policy.mjs';
@@ -18,17 +18,17 @@ export const CAPABILITIES_SCHEMA_VERSION = 1;
18
18
 
19
19
  // Each command, what it takes and what it writes.
20
20
  export const COMMANDS = {
21
- scan: { args: ['<path>'], options: ['--format', '--out', '--repos', '--only', '--quiet', '--full-trace', '--all-routes', '--baseline', '--no-baseline', '--advisories', '--copylib', '--pds-export', '--evidence'], documents: ['cobolwork', 'sarif'] },
21
+ scan: { args: ['<path>'], options: ['--format', '--out', '--repos', '--only', '--quiet', '--full-trace', '--all-routes', '--baseline', '--no-baseline', '--advisories', '--copylib', '--pds-export', '--mvs38-forms', '--no-mvs38-forms', '--hlasm-optable', '--evidence'], documents: ['cobolwork', 'sarif'] },
22
22
  flow: { args: ['<path>'], options: ['--out', '--repos', '--quiet', '--full-trace', '--all-routes', '--baseline', '--no-baseline', '--copylib', '--evidence'], documents: ['cobolwork-flow'] },
23
23
  inventory: { args: ['<path>'], options: ['--out', '--quiet', '--copylib', '--evidence'], documents: ['cobolwork-inventory'] },
24
24
  parse: { args: ['<file>'], options: ['--out'], documents: ['cobolwork-parse'] },
25
25
  sbom: { args: ['<path>'], options: ['--name', '--out', '--quiet', '--copylib', '--evidence'], documents: ['cyclonedx'] },
26
- diff: { args: ['<repo>'], options: ['--base', '--head', '--format', '--out', '--only', '--quiet', '--full-trace', '--copylib', '--evidence'], documents: ['cobolwork-diff', 'sarif'] },
26
+ diff: { args: ['<repo>'], options: ['--base', '--head', '--format', '--out', '--only', '--quiet', '--full-trace', '--copylib', '--pds-export', '--evidence'], documents: ['cobolwork-diff', 'sarif'] },
27
27
  baseline: { args: ['<path>'], options: ['--reason', '--who', '--expires', '--action', '--rule', '--out', '--repos', '--only', '--advisories', '--copylib', '--evidence'], documents: ['cobolwork-baseline'] },
28
28
  tui: { args: ['[path]'], options: ['--report', '--keys', '--baseline', '--no-baseline', '--advisories', '--copylib'], documents: [] },
29
29
  explain: { args: ['<path>', '<fingerprint>'], options: ['--report', '--out', '--baseline', '--no-baseline', '--advisories', '--copylib'], documents: ['cobolwork-explain'] },
30
30
  gate: { args: ['<repo>'], options: ['--base', '--head', '--target', '--cobc', '--exit-code', '--target-only', '--out', '--evidence'], documents: ['cobolwork-gate'] },
31
- build: { args: ['<repo>', '[-- <compiler> <arg>...]'], options: ['--base', '--head', '--policy', '--provenance', '--provenance-format', '--artifact', '--equivalence', '--allowed-signers', '--ironwork', '--advisories', '--copylib', '--no-baseline', '--format', '--out', '--evidence'], documents: ['cobolwork-build', 'cobolwork-build-provenance', 'sarif'] },
31
+ build: { args: ['<repo>', '[-- <compiler> <arg>...]'], options: ['--base', '--head', '--policy', '--provenance', '--provenance-format', '--artifact', '--equivalence', '--allowed-signers', '--ironwork', '--precompile', '--advisories', '--copylib', '--pds-export', '--no-baseline', '--format', '--out', '--evidence'], documents: ['cobolwork-build', 'cobolwork-build-provenance', 'sarif'] },
32
32
  evidence: { args: ['verify|seal|anchor|sign', '[statement]'], options: ['--evidence', '--ssh-key', '--signer', '--expect-key', '--allowed-signers', '--anchor-git', '--ref', '--anchor-pin', '--push', '--max-unsealed', '--tsq', '--tsr', '--tsa-ca', '--cosign-bundle', '--cosign-key', '--certificate-identity', '--certificate-oidc-issuer', '--trusted-root', '--insecure-ignore-tlog', '--out', '--quiet'], documents: ['cobolwork-evidence'] },
33
33
  capabilities: { args: [], options: ['--json'], documents: ['cobolwork-capabilities'] },
34
34
  };
@@ -44,7 +44,6 @@ export function capabilities() {
44
44
  toolRevision: toolRevision(),
45
45
  commands: COMMANDS,
46
46
  globalOptions: GLOBAL_OPTIONS,
47
- // A document whose schemaVersion is null carries none yet; a reader should key on `tool`.
48
47
  documents: {
49
48
  cobolwork: SCHEMA_VERSION,
50
49
  'cobolwork-flow': SCHEMA_VERSION,
@@ -54,10 +53,10 @@ export function capabilities() {
54
53
  'cobolwork-build': BUILD_SCHEMA_VERSION,
55
54
  'cobolwork-build-provenance': BUILD_SCHEMA_VERSION,
56
55
  'cobolwork-capabilities': CAPABILITIES_SCHEMA_VERSION,
57
- 'cobolwork-explain': null,
58
- 'cobolwork-baseline': null,
59
- 'cobolwork-parse': null,
60
- 'cobolwork-evidence': null,
56
+ 'cobolwork-explain': EXPLAIN_SCHEMA_VERSION,
57
+ 'cobolwork-baseline': BASELINE_RESULT_SCHEMA_VERSION,
58
+ 'cobolwork-parse': PARSE_SCHEMA_VERSION,
59
+ 'cobolwork-evidence': EVIDENCE_RESULT_SCHEMA_VERSION,
61
60
  cyclonedx: '1.6',
62
61
  sarif: '2.1.0',
63
62
  },
@@ -69,7 +68,7 @@ export function capabilities() {
69
68
  jcl: JCL_EXT,
70
69
  bms: BMS_EXT,
71
70
  extensionless: 'sniffed by content: a file with no extension is classified by what it holds',
72
- pdsExport: 'scan --pds-export: every member classified by content, whatever extension the download gave it; text, --binary and --record downloads',
71
+ pdsExport: 'scan --pds-export: every member classified by content, whatever extension the download gave it; text, --binary and --record downloads; XMIT files and IEBCOPY unloads of a PDS or a sequential data set',
73
72
  zowe: ['zowe.config.json', 'zowe.config.user.json', '.mcp.json', '.vscode/mcp.json', '.cursor/mcp.json', '.claude/settings.json', '.claude/settings.local.json', '.gemini/settings.json', 'claude_desktop_config.json', '.vscode/settings.json', 'systems.json under a mock directory'],
74
73
  },
75
74
  evidenceKinds: Object.keys(EVIDENCE),