codictator 0.1.2 → 0.2.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 (71) hide show
  1. package/dist/builtin-packs/core/fixtures/conventional-commit/passing/basic/case.json +1 -1
  2. package/dist/builtin-packs/core/fixtures/conventional-commit/violating/basic/case.json +1 -1
  3. package/dist/builtin-packs/typescript/fixtures/lint/passing/exported/case.json +8 -0
  4. package/dist/builtin-packs/typescript/pack.json +1 -1
  5. package/dist/cli/commands.d.ts +1 -1
  6. package/dist/cli/commands.js +3 -1
  7. package/dist/cli/gate-scope.d.ts +1 -0
  8. package/dist/cli/gate-scope.js +11 -0
  9. package/dist/cli/git-hook.js +13 -8
  10. package/dist/cli/guardrail.d.ts +2 -0
  11. package/dist/cli/guardrail.js +11 -13
  12. package/dist/cli/init-plan.d.ts +7 -0
  13. package/dist/cli/init-plan.js +21 -2
  14. package/dist/cli/init.d.ts +2 -0
  15. package/dist/cli/init.js +8 -17
  16. package/dist/cli/plan-launcher.d.ts +1 -0
  17. package/dist/cli/plan-launcher.js +53 -3
  18. package/dist/cli/serve-run.js +1 -0
  19. package/dist/cli/setup-gitignore.d.ts +7 -0
  20. package/dist/cli/setup-gitignore.js +41 -0
  21. package/dist/cli/setup-handoff.d.ts +7 -0
  22. package/dist/cli/setup-handoff.js +44 -0
  23. package/dist/cli/setup-install.d.ts +11 -0
  24. package/dist/cli/setup-install.js +84 -0
  25. package/dist/cli/setup.d.ts +9 -0
  26. package/dist/cli/setup.js +143 -0
  27. package/dist/cli/task-advance.js +16 -16
  28. package/dist/features/adapters/claude-shaped.d.ts +1 -0
  29. package/dist/features/adapters/claude-shaped.js +29 -31
  30. package/dist/features/adapters/copilot.js +22 -29
  31. package/dist/features/audit/append.js +9 -4
  32. package/dist/features/audit/surfaces.js +15 -8
  33. package/dist/features/ci/protection.js +37 -13
  34. package/dist/features/deps/npm.js +39 -24
  35. package/dist/features/deps/registry.js +45 -34
  36. package/dist/features/deps/stable.js +52 -27
  37. package/dist/features/deps/upgrade.d.ts +1 -1
  38. package/dist/features/deps/upgrade.js +29 -19
  39. package/dist/features/engine/evaluate.js +6 -1
  40. package/dist/features/packs/enforcers/ci-guards.js +4 -1
  41. package/dist/features/packs/enforcers/code.js +2 -1
  42. package/dist/features/packs/enforcers/engine-version-guards.d.ts +6 -0
  43. package/dist/features/packs/enforcers/engine-version-guards.js +42 -0
  44. package/dist/features/packs/enforcers/external-bind.js +62 -28
  45. package/dist/features/packs/enforcers/external-sarif.js +30 -24
  46. package/dist/features/packs/enforcers/gate-guards.d.ts +0 -1
  47. package/dist/features/packs/enforcers/gate-guards.js +83 -136
  48. package/dist/features/packs/enforcers/health-measures.d.ts +2 -0
  49. package/dist/features/packs/enforcers/health-measures.js +10 -1
  50. package/dist/features/packs/enforcers/health-refs.d.ts +1 -2
  51. package/dist/features/packs/enforcers/health-refs.js +71 -56
  52. package/dist/features/packs/enforcers/secret-scan.d.ts +4 -20
  53. package/dist/features/packs/enforcers/secret-scan.js +61 -67
  54. package/dist/features/packs/enforcers/ts-checks.js +104 -58
  55. package/dist/features/packs/enforcers/ts-security-patterns.js +12 -8
  56. package/dist/features/packs/manifest-decl.d.ts +13 -0
  57. package/dist/features/packs/manifest-decl.js +121 -0
  58. package/dist/features/packs/manifest.d.ts +2 -12
  59. package/dist/features/packs/manifest.js +5 -90
  60. package/dist/features/packs/selftest.js +22 -5
  61. package/dist/features/remediation/adoption.js +40 -29
  62. package/dist/features/testing/scenarios.js +49 -27
  63. package/dist/features/testing/tap-parser.js +38 -26
  64. package/dist/features/workflow/phases.js +45 -42
  65. package/dist/features/workflow/review.js +17 -10
  66. package/dist/kernel/git.js +4 -1
  67. package/dist/kernel/pattern.js +120 -86
  68. package/dist/kernel/secret-paths.js +34 -20
  69. package/dist/kernel/shell-cmd.js +23 -6
  70. package/dist/tsconfig.build.tsbuildinfo +1 -1
  71. package/package.json +1 -1
@@ -14,24 +14,37 @@ export const DEFAULT_DEP_LAG_PARAMS = {
14
14
  // SPEC §6.1: an exact pin is a full x.y.z with an optional -pre suffix;
15
15
  // compare follows npm version order (numeric parts first, release beats
16
16
  // prerelease, prerelease identifiers numeric then lexical).
17
+ // The pattern lives at module scope so its quantifiers never count as
18
+ // branches of the function that applies it.
19
+ const NPM_VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
20
+ function versionPart(match, index) {
21
+ return Number(match[index] ?? "");
22
+ }
23
+ function narrowVersionParts(match) {
24
+ const major = versionPart(match, 1);
25
+ const minor = versionPart(match, 2);
26
+ const patch = versionPart(match, 3);
27
+ if (!Number.isSafeInteger(major) ||
28
+ !Number.isSafeInteger(minor) ||
29
+ !Number.isSafeInteger(patch)) {
30
+ return undefined;
31
+ }
32
+ return { major, minor, patch };
33
+ }
17
34
  export function parseNpmVersion(value) {
18
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
35
+ const match = NPM_VERSION_PATTERN.exec(value);
19
36
  if (match === null) {
20
37
  return undefined;
21
38
  }
22
- const major = Number(match[1] ?? "");
23
- const minor = Number(match[2] ?? "");
24
- const patch = Number(match[3] ?? "");
25
- if (!Number.isSafeInteger(major) ||
26
- !Number.isSafeInteger(minor) ||
27
- !Number.isSafeInteger(patch)) {
39
+ const parts = narrowVersionParts(match);
40
+ if (parts === undefined) {
28
41
  return undefined;
29
42
  }
30
43
  const pre = match[4];
31
44
  if (pre === undefined || pre === "") {
32
- return { major, minor, patch, prerelease: undefined };
45
+ return { ...parts, prerelease: undefined };
33
46
  }
34
- return { major, minor, patch, prerelease: pre.split(".") };
47
+ return { ...parts, prerelease: pre.split(".") };
35
48
  }
36
49
  export function isNpmPrerelease(value) {
37
50
  const parsed = parseNpmVersion(value);
@@ -65,12 +78,13 @@ function compareIdentifiers(left, right) {
65
78
  }
66
79
  return 0;
67
80
  }
68
- export function compareNpmVersions(left, right) {
69
- for (const pair of [
81
+ function compareVersionCore(left, right) {
82
+ const pairs = [
70
83
  [left.major, right.major],
71
84
  [left.minor, right.minor],
72
85
  [left.patch, right.patch],
73
- ]) {
86
+ ];
87
+ for (const pair of pairs) {
74
88
  if (pair[0] < pair[1]) {
75
89
  return -1;
76
90
  }
@@ -78,29 +92,40 @@ export function compareNpmVersions(left, right) {
78
92
  return 1;
79
93
  }
80
94
  }
81
- if (left.prerelease === undefined && right.prerelease === undefined) {
82
- return 0;
95
+ return 0;
96
+ }
97
+ function prereleaseOrderAt(left, right, index) {
98
+ const leftId = left[index];
99
+ const rightId = right[index];
100
+ if (leftId === undefined) {
101
+ return -1;
83
102
  }
84
- if (left.prerelease === undefined) {
103
+ if (rightId === undefined) {
85
104
  return 1;
86
105
  }
87
- if (right.prerelease === undefined) {
106
+ const order = compareIdentifiers(leftId, rightId);
107
+ return order === 0 ? undefined : order;
108
+ }
109
+ function comparePrerelease(left, right) {
110
+ if (left === undefined) {
111
+ return right === undefined ? 0 : 1;
112
+ }
113
+ if (right === undefined) {
88
114
  return -1;
89
115
  }
90
- const length = Math.max(left.prerelease.length, right.prerelease.length);
116
+ const length = Math.max(left.length, right.length);
91
117
  for (let index = 0; index < length; index += 1) {
92
- const leftId = left.prerelease[index];
93
- const rightId = right.prerelease[index];
94
- if (leftId === undefined) {
95
- return -1;
96
- }
97
- if (rightId === undefined) {
98
- return 1;
99
- }
100
- const order = compareIdentifiers(leftId, rightId);
101
- if (order !== 0) {
118
+ const order = prereleaseOrderAt(left, right, index);
119
+ if (order !== undefined) {
102
120
  return order;
103
121
  }
104
122
  }
105
123
  return 0;
106
124
  }
125
+ export function compareNpmVersions(left, right) {
126
+ const core = compareVersionCore(left, right);
127
+ if (core !== 0) {
128
+ return core;
129
+ }
130
+ return comparePrerelease(left.prerelease, right.prerelease);
131
+ }
@@ -30,6 +30,6 @@ export interface UpgradeHost {
30
30
  readonly push: (branch: string) => Promise<Result<void>>;
31
31
  }
32
32
  export declare function upgradeCommitSubject(name: string, from: string, to: string): string;
33
- export declare function upgradeHistoryEntry(date: string, subject: string, name: string, from: string, to: string, task: string, advisoryIds: ReadonlyArray<string>): string;
33
+ export declare function upgradeHistoryEntry(date: string, commitSubject: string, name: string, from: string, to: string, task: string, advisoryIds: ReadonlyArray<string>): string;
34
34
  export declare function planUpgradePr(input: UpgradePrInput): Result<UpgradePrPlan>;
35
35
  export declare function runUpgradePr(plan: UpgradePrPlan, host: UpgradeHost): Promise<Result<UpgradePrPlan>>;
@@ -12,10 +12,10 @@ export function upgradeCommitSubject(name, from, to) {
12
12
  }
13
13
  // SPEC §7.7: a deps entry carries Kind, Task, Why and one Deps line per
14
14
  // change in the added, upgraded or removed form.
15
- export function upgradeHistoryEntry(date, subject, name, from, to, task, advisoryIds) {
15
+ export function upgradeHistoryEntry(date, commitSubject, name, from, to, task, advisoryIds) {
16
16
  const rendered = advisoryIds.length === 0 ? "none" : [...advisoryIds].sort().join(", ");
17
17
  return [
18
- `## ${date} · ${subject}`,
18
+ `## ${date} · ${commitSubject}`,
19
19
  `- **Kind:** deps`,
20
20
  `- **Task:** ${task}`,
21
21
  `- **Why:** L5 sweep upgrades ${name} to latest stable ${to} (SPEC §4.8.7).`,
@@ -54,25 +54,19 @@ function appendHistoryEntry(historyText, date, entry) {
54
54
  }
55
55
  return `${title}\n\n${entry}\n\n${rest.endsWith("\n") ? rest : `${rest}\n`}`;
56
56
  }
57
- // SPEC §4.8.7: the PR holds the manifest edit, the regenerated lockfile and
58
- // its history entry. The lockfile bytes come from the caller (regenerated by
59
- // the ecosystem tool); without them the plan holds manifest plus history.
60
- export function planUpgradePr(input) {
61
- if (!/^\d{4}-\d{2}-\d{2}$/.test(input.date)) {
62
- return {
63
- ok: false,
64
- error: makeError("CDX-E903", `upgrade date ${input.date} is not a UTC YYYY-MM-DD date`, "Pass the commit date in UTC as YYYY-MM-DD."),
65
- };
57
+ function checkUpgradeDate(date) {
58
+ if (/^\d{4}-\d{2}-\d{2}$/.test(date)) {
59
+ return { ok: true, value: date };
66
60
  }
67
- const bumped = bumpManifestPin(input.manifestText, input.name, input.from, input.to);
68
- if (!bumped.ok) {
69
- return bumped;
70
- }
71
- const subject = upgradeCommitSubject(input.name, input.from, input.to);
72
- const entry = upgradeHistoryEntry(input.date, subject, input.name, input.from, input.to, input.task ?? "none", input.advisoryIds ?? []);
61
+ return {
62
+ ok: false,
63
+ error: makeError("CDX-E903", `upgrade date ${date} is not a UTC YYYY-MM-DD date`, "Pass the commit date in UTC as YYYY-MM-DD."),
64
+ };
65
+ }
66
+ function upgradePlanFiles(input, manifestText, entry) {
73
67
  const historyPath = input.historyPath ?? `docs/HISTORY/${input.date.slice(0, 10)}.md`;
74
68
  const files = [
75
- { path: input.manifestPath, text: bumped.value },
69
+ { path: input.manifestPath, text: manifestText },
76
70
  {
77
71
  path: historyPath,
78
72
  text: appendHistoryEntry(input.historyText, input.date, entry),
@@ -81,12 +75,28 @@ export function planUpgradePr(input) {
81
75
  if (input.lockfilePath !== undefined && input.lockfileText !== undefined) {
82
76
  files.push({ path: input.lockfilePath, text: input.lockfileText });
83
77
  }
78
+ return files;
79
+ }
80
+ // SPEC §4.8.7: the PR holds the manifest edit, the regenerated lockfile and
81
+ // its history entry. The lockfile bytes come from the caller (regenerated by
82
+ // the ecosystem tool); without them the plan holds manifest plus history.
83
+ export function planUpgradePr(input) {
84
+ const dated = checkUpgradeDate(input.date);
85
+ if (!dated.ok) {
86
+ return dated;
87
+ }
88
+ const bumped = bumpManifestPin(input.manifestText, input.name, input.from, input.to);
89
+ if (!bumped.ok) {
90
+ return bumped;
91
+ }
92
+ const subject = upgradeCommitSubject(input.name, input.from, input.to);
93
+ const entry = upgradeHistoryEntry(input.date, subject, input.name, input.from, input.to, input.task ?? "none", input.advisoryIds ?? []);
84
94
  return {
85
95
  ok: true,
86
96
  value: {
87
97
  branch: upgradeBranchName(input.ecosystem, input.name, input.to),
88
98
  subject,
89
- files,
99
+ files: upgradePlanFiles(input, bumped.value, entry),
90
100
  },
91
101
  };
92
102
  }
@@ -93,7 +93,12 @@ function ruleIdsOf(findings) {
93
93
  }
94
94
  async function collectFindings(ordered, ctx, store) {
95
95
  const runnable = ordered.filter((enforcer) => enforcer.kinds.includes(ctx.trigger));
96
- const ran = await Promise.all(runnable.map((enforcer) => runEnforcer(enforcer, ctx)));
96
+ // SPEC §4.3.3 step 2: bounded concurrency — one guard at a time keeps every
97
+ // guard's event-loop share (no E902 herd); sortFindings keeps the verdict byte-identical (§4.3.6).
98
+ const ran = [];
99
+ for (const enforcer of runnable) {
100
+ ran.push(await runEnforcer(enforcer, ctx));
101
+ }
97
102
  return applyWaivers(sortFindings(ran.flat()), store, ctx.now);
98
103
  }
99
104
  function decideDecision(kept, trigger) {
@@ -110,7 +110,10 @@ export function resolveConventionalCommit(decl, rule, ports) {
110
110
  const findings = [];
111
111
  for (const file of selectFiles(ctx.scope.files, binding.scopeGlobs)) {
112
112
  const text = await loadScopeText(ports, binding, file, findings);
113
- if (text === undefined) {
113
+ // Commit subjects arrive as marker reports (SPEC §4.4.1
114
+ // sample.txt fixtures); source files carry `subject:` lines
115
+ // of their own (parameters, YAML keys), which are not commits.
116
+ if (text === undefined || !isMarkerFile(file)) {
114
117
  continue;
115
118
  }
116
119
  const subject = parseMarkers(text).get("subject") ?? "";
@@ -6,7 +6,8 @@ import { resolveDepExactPin, resolveDepLatestStable, resolveDepLockfile, resolve
6
6
  import { resolveDepLicense, resolveDepNoDeprecated, resolveDepNoVulnerable, resolveDepOnePerCapability, } from "./deps-policy-guards.js";
7
7
  import { resolveDocNoCorpusCounts, resolveDocOneLevel, resolveDocPlacement, resolveDocRouterLeaf, resolveDocSingleOwner, } from "./docs-guards.js";
8
8
  import { resolveDocBreadth, resolveDocDensity, resolveDocLinksValid, resolveDocNoInlineChangelog, } from "./docs-guards-measures.js";
9
- import { resolveAgentAssetsPresent, resolveCustomPackSelftest, resolveEngineVersionMatch, resolveGuardrailManifest, resolveHooksActive, resolvePerfBudgets, resolveWaiversValid, } from "./gate-guards.js";
9
+ import { resolveEngineVersionMatch } from "./engine-version-guards.js";
10
+ import { resolveAgentAssetsPresent, resolveCustomPackSelftest, resolveGuardrailManifest, resolveHooksActive, resolvePerfBudgets, resolveWaiversValid, } from "./gate-guards.js";
10
11
  import { resolveNoSpeculativeAbstraction } from "./health-abstraction.js";
11
12
  import { resolveDuplication } from "./health-duplication.js";
12
13
  import { resolveDeadCode } from "./health-refs.js";
@@ -0,0 +1,6 @@
1
+ import type { Ports } from "../../../kernel/index.ts";
2
+ import { type Result } from "../../../kernel/index.ts";
3
+ import type { Enforcer } from "../../engine/index.ts";
4
+ import type { PackRule } from "../index.ts";
5
+ import type { EnforcerDecl } from "../manifest.ts";
6
+ export declare function resolveEngineVersionMatch(decl: EnforcerDecl, rule: PackRule, ports: Ports): Result<Enforcer>;
@@ -0,0 +1,42 @@
1
+ import { parseJson } from "../../../kernel/index.js";
2
+ import { findingFor, readErrorFinding, selectFiles, } from "./binding.js";
3
+ import { checkEnforcer, loadScopeText } from "./health-measures.js";
4
+ async function checkEngineVersionFile(ports, binding, file, findings) {
5
+ const text = await loadScopeText(ports, binding, file, findings);
6
+ if (text === undefined) {
7
+ return;
8
+ }
9
+ const parsed = parseJson(text);
10
+ if (!parsed.ok) {
11
+ findings.push(readErrorFinding(binding, file, parsed.error, "parse"));
12
+ return;
13
+ }
14
+ const record = (typeof parsed.value === "object" && parsed.value !== null
15
+ ? parsed.value
16
+ : {});
17
+ if (typeof record.engine !== "string" ||
18
+ typeof record.pack !== "string" ||
19
+ record.engine === record.pack) {
20
+ return;
21
+ }
22
+ findings.push(findingFor(binding, {
23
+ path: file,
24
+ message: `pack version ${record.pack} does not match the engine ${record.engine}`,
25
+ vars: { path: file, value: record.engine },
26
+ }));
27
+ }
28
+ async function checkEngineVersionFiles(ports, binding, ctx) {
29
+ const findings = [];
30
+ for (const file of selectFiles(ctx.scope.files, binding.scopeGlobs)) {
31
+ if (file !== "engine.json" && !file.endsWith("/engine.json")) {
32
+ continue;
33
+ }
34
+ await checkEngineVersionFile(ports, binding, file, findings);
35
+ }
36
+ return findings;
37
+ }
38
+ // SPEC §4.14.2 core/engine-version-match: the declared version must equal
39
+ // the installed engine; a pack ahead of the engine flags.
40
+ export function resolveEngineVersionMatch(decl, rule, ports) {
41
+ return checkEnforcer(decl, rule, (binding, ctx) => checkEngineVersionFiles(ports, binding, ctx));
42
+ }
@@ -15,17 +15,20 @@ function stringList(value) {
15
15
  }
16
16
  return undefined;
17
17
  }
18
- // SPEC §7.2: a tools entry needs an id, an argv command and an exact pin
19
- // with its probe. CDX-E206 when the pin is missing (§8).
20
- export function narrowToolDecl(entry) {
21
- if (!isRecord(entry)) {
22
- return toolError("CDX-E203", "a config.tools entry must be an object");
23
- }
18
+ function narrowTimeoutMs(timeout) {
19
+ return typeof timeout === "number" && Number.isInteger(timeout) && timeout > 0
20
+ ? timeout
21
+ : DEFAULT_TIMEOUT_MS;
22
+ }
23
+ function narrowToolIdentity(entry) {
24
24
  const id = field(entry, "id");
25
25
  const command = stringList(field(entry, "command")) ?? [];
26
26
  if (typeof id !== "string" || id.length === 0 || command.length === 0) {
27
27
  return toolError("CDX-E203", "a config.tools entry needs a non-empty id and command argv");
28
28
  }
29
+ return ok({ id, command });
30
+ }
31
+ function narrowToolVersion(entry, id) {
29
32
  const version = field(entry, "version");
30
33
  if (!isRecord(version)) {
31
34
  return toolError("CDX-E206", `tool "${id}" has no exact version pin`);
@@ -41,47 +44,78 @@ export function narrowToolDecl(entry) {
41
44
  pattern.length === 0) {
42
45
  return toolError("CDX-E203", `tool "${id}" needs a probe argv and a pattern`);
43
46
  }
47
+ return ok({ pin, probe, pattern });
48
+ }
49
+ // SPEC §7.2: a tools entry needs an id, an argv command and an exact pin
50
+ // with its probe. CDX-E206 when the pin is missing (§8).
51
+ export function narrowToolDecl(entry) {
52
+ if (!isRecord(entry)) {
53
+ return toolError("CDX-E203", "a config.tools entry must be an object");
54
+ }
55
+ const identity = narrowToolIdentity(entry);
56
+ if (!identity.ok) {
57
+ return identity;
58
+ }
59
+ const version = narrowToolVersion(entry, identity.value.id);
60
+ if (!version.ok) {
61
+ return version;
62
+ }
44
63
  const role = field(entry, "role");
45
64
  if (typeof role === "string" && role !== "enforcer") {
46
- return toolError("CDX-E203", `tool "${id}" is a helper and cannot back an enforcer`);
65
+ return toolError("CDX-E203", `tool "${identity.value.id}" is a helper and cannot back an enforcer`);
47
66
  }
48
67
  const timeout = field(entry, "timeoutMs");
49
68
  const envPass = stringList(field(entry, "envPass")) ?? [];
50
69
  return ok({
51
- id,
52
- command,
53
- version: { pin, probe, pattern },
70
+ id: identity.value.id,
71
+ command: identity.value.command,
72
+ version: version.value,
54
73
  role: typeof role === "string" ? role : undefined,
55
- timeoutMs: typeof timeout === "number" && Number.isInteger(timeout) && timeout > 0
56
- ? timeout
57
- : DEFAULT_TIMEOUT_MS,
74
+ timeoutMs: narrowTimeoutMs(timeout),
58
75
  envPass,
59
76
  });
60
77
  }
78
+ function narrowOutputFormat(value, withId) {
79
+ if (value === "sarif" ||
80
+ value === "json" ||
81
+ value === "regex" ||
82
+ value === "exit-code") {
83
+ return ok(value);
84
+ }
85
+ return withId("needs an output format of sarif, json, regex or exit-code");
86
+ }
87
+ function narrowOutputSource(value, withId) {
88
+ const source = value ?? "stdout";
89
+ if (source === "stdout" || source === "{out}") {
90
+ return ok(source);
91
+ }
92
+ return withId("needs an output source of stdout or {out}");
93
+ }
61
94
  function narrowOutput(declId, value) {
62
95
  if (!isRecord(value)) {
63
96
  return toolError(declId, "needs an output binding");
64
97
  }
65
98
  const withId = (detail) => toolError("CDX-E203", `pack enforcer "${declId}" ${detail}`);
66
- const format = field(value, "format");
67
- if (format !== "sarif" &&
68
- format !== "json" &&
69
- format !== "regex" &&
70
- format !== "exit-code") {
71
- return withId("needs an output format of sarif, json, regex or exit-code");
99
+ const format = narrowOutputFormat(field(value, "format"), withId);
100
+ if (!format.ok) {
101
+ return format;
72
102
  }
73
- const source = field(value, "source") ?? "stdout";
74
- if (source !== "stdout" && source !== "{out}") {
75
- return withId("needs an output source of stdout or {out}");
103
+ const source = narrowOutputSource(field(value, "source"), withId);
104
+ if (!source.ok) {
105
+ return source;
76
106
  }
77
- const output = source === "{out}" ? "{out}" : "stdout";
78
- if (format === "json") {
79
- return narrowJsonSelect(withId, format, output, field(value, "select"));
107
+ if (format.value === "json") {
108
+ return narrowJsonSelect(withId, format.value, source.value, field(value, "select"));
80
109
  }
81
- if (format === "regex") {
82
- return narrowRegexPattern(withId, format, output, field(value, "pattern"));
110
+ if (format.value === "regex") {
111
+ return narrowRegexPattern(withId, format.value, source.value, field(value, "pattern"));
83
112
  }
84
- return ok({ format, source: output, select: undefined, pattern: undefined });
113
+ return ok({
114
+ format: format.value,
115
+ source: source.value,
116
+ select: undefined,
117
+ pattern: undefined,
118
+ });
85
119
  }
86
120
  function narrowJsonSelect(withId, format, output, select) {
87
121
  if (!isRecord(select)) {
@@ -59,6 +59,35 @@ function sarifLocation(result) {
59
59
  }
60
60
  return { path: uri, line };
61
61
  }
62
+ function collectSarifEntry(entry, run, findings, escapes) {
63
+ if (!isRecord(entry)) {
64
+ return;
65
+ }
66
+ const toolRule = sarifRuleId(entry, run);
67
+ if (toolRule === undefined) {
68
+ return;
69
+ }
70
+ const location = sarifLocation(entry);
71
+ const detail = field(entry, "message");
72
+ pushEntry(findings, escapes, {
73
+ toolRule,
74
+ path: location.path,
75
+ line: location.line,
76
+ message: isRecord(detail) ? field(detail, "text") : undefined,
77
+ });
78
+ }
79
+ function collectSarifRun(run, findings, escapes) {
80
+ if (!isRecord(run)) {
81
+ return;
82
+ }
83
+ const results = field(run, "results");
84
+ if (!Array.isArray(results)) {
85
+ return;
86
+ }
87
+ for (const entry of results) {
88
+ collectSarifEntry(entry, run, findings, escapes);
89
+ }
90
+ }
62
91
  export function parseSarif(text) {
63
92
  const parsed = parseJson(text);
64
93
  if (!parsed.ok) {
@@ -74,30 +103,7 @@ export function parseSarif(text) {
74
103
  const findings = [];
75
104
  const escapes = { count: 0 };
76
105
  for (const run of runs) {
77
- if (!isRecord(run)) {
78
- continue;
79
- }
80
- const results = field(run, "results");
81
- if (!Array.isArray(results)) {
82
- continue;
83
- }
84
- for (const entry of results) {
85
- if (!isRecord(entry)) {
86
- continue;
87
- }
88
- const toolRule = sarifRuleId(entry, run);
89
- if (toolRule === undefined) {
90
- continue;
91
- }
92
- const location = sarifLocation(entry);
93
- const detail = field(entry, "message");
94
- pushEntry(findings, escapes, {
95
- toolRule,
96
- path: location.path,
97
- line: location.line,
98
- message: isRecord(detail) ? field(detail, "text") : undefined,
99
- });
100
- }
106
+ collectSarifRun(run, findings, escapes);
101
107
  }
102
108
  return ok({ findings, escapes: escapes.count });
103
109
  }
@@ -7,6 +7,5 @@ export declare function resolveAgentAssetsPresent(decl: EnforcerDecl, rule: Pack
7
7
  export declare function resolvePerfBudgets(decl: EnforcerDecl, rule: PackRule, ports: Ports): Result<Enforcer>;
8
8
  export declare function resolveGuardrailManifest(decl: EnforcerDecl, rule: PackRule, ports: Ports): Result<Enforcer>;
9
9
  export declare function resolveHooksActive(decl: EnforcerDecl, rule: PackRule, ports: Ports): Result<Enforcer>;
10
- export declare function resolveEngineVersionMatch(decl: EnforcerDecl, rule: PackRule, ports: Ports): Result<Enforcer>;
11
10
  export declare function resolveWaiversValid(decl: EnforcerDecl, rule: PackRule, ports: Ports): Result<Enforcer>;
12
11
  export declare function resolveCustomPackSelftest(decl: EnforcerDecl, rule: PackRule, ports: Ports): Result<Enforcer>;