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
@@ -1,6 +1,20 @@
1
- import { ok, parseJson } from "../../../kernel/index.js";
2
- import { bindRule, findingFor, readErrorFinding, selectFiles, tunableStringList, } from "./binding.js";
3
- import { baseEnforcer, loadScopeText, parseMarkerInt, scopeMarkerTexts, } from "./health-measures.js";
1
+ import { ok } from "../../../kernel/index.js";
2
+ import { bindRule, findingFor, tunableStringList, } from "./binding.js";
3
+ import { baseEnforcer, parseMarkerInt, scopeMarkerTexts, } from "./health-measures.js";
4
+ // The marker-driven guards below share one scope loop: bind the rule once,
5
+ // walk the marker reports, and let each guard judge one file's markers. The
6
+ // loop lives here so the resolvers hold only their own verdict and the
7
+ // shared run the duplication gate flags stays in one place.
8
+ function markerGuard(decl, rule, ports, judge) {
9
+ const binding = bindRule(rule);
10
+ return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
11
+ const findings = [];
12
+ for (const { file, markers } of await scopeMarkerTexts(ports, binding, ctx, findings)) {
13
+ judge(binding, file, markers, findings);
14
+ }
15
+ return findings;
16
+ }));
17
+ }
4
18
  // SPEC §4.6.6 core/agent-assets-present: every required helper must be
5
19
  // present; the tunable defaults to knowledge-graph and token-saver (§5.12).
6
20
  export function resolveAgentAssetsPresent(decl, rule, ports) {
@@ -9,162 +23,95 @@ export function resolveAgentAssetsPresent(decl, rule, ports) {
9
23
  "knowledge-graph",
10
24
  "token-saver",
11
25
  ];
12
- return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
13
- const findings = [];
14
- for (const { file, markers } of await scopeMarkerTexts(ports, binding, ctx, findings)) {
15
- const present = new Set((markers.get("helpers") ?? "")
16
- .split(",")
17
- .map((entry) => entry.trim())
18
- .filter((entry) => entry !== ""));
19
- const missing = required.filter((name) => !present.has(name));
20
- if (missing.length > 0) {
21
- findings.push(findingFor(binding, {
22
- path: file,
23
- message: `required helper assets missing: ${missing.join(", ")}`,
24
- vars: { path: file, detail: missing.join(", ") },
25
- }));
26
- }
26
+ return markerGuard(decl, rule, ports, (guard, file, markers, findings) => {
27
+ const present = new Set((markers.get("helpers") ?? "")
28
+ .split(",")
29
+ .map((entry) => entry.trim())
30
+ .filter((entry) => entry !== ""));
31
+ const missing = required.filter((name) => !present.has(name));
32
+ if (missing.length > 0) {
33
+ findings.push(findingFor(guard, {
34
+ path: file,
35
+ message: `required helper assets missing: ${missing.join(", ")}`,
36
+ vars: { path: file, detail: missing.join(", ") },
37
+ }));
27
38
  }
28
- return findings;
29
- }));
39
+ });
30
40
  }
31
41
  // SPEC §9.3 core/perf-budgets: a measured p95 past the declared budget
32
42
  // flags; inside the budget stays quiet.
33
43
  export function resolvePerfBudgets(decl, rule, ports) {
34
- const binding = bindRule(rule);
35
- return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
36
- const findings = [];
37
- for (const { file, markers } of await scopeMarkerTexts(ports, binding, ctx, findings)) {
38
- const rawP95 = markers.get("hook-p95-ms") ?? "";
39
- const rawBudget = markers.get("budget-ms") ?? "";
40
- const p95 = Number.parseFloat(rawP95);
41
- const budget = Number.parseFloat(rawBudget);
42
- if (Number.isFinite(p95) && Number.isFinite(budget) && p95 > budget) {
43
- findings.push(findingFor(binding, {
44
+ return markerGuard(decl, rule, ports, (guard, file, markers, findings) => {
45
+ const rawP95 = markers.get("hook-p95-ms") ?? "";
46
+ const rawBudget = markers.get("budget-ms") ?? "";
47
+ const p95 = Number.parseFloat(rawP95);
48
+ const budget = Number.parseFloat(rawBudget);
49
+ if (Number.isFinite(p95) && Number.isFinite(budget) && p95 > budget) {
50
+ findings.push(findingFor(guard, {
51
+ path: file,
52
+ message: `hook p95 of ${rawP95}ms is over the ${rawBudget}ms budget`,
53
+ vars: {
44
54
  path: file,
45
- message: `hook p95 of ${rawP95}ms is over the ${rawBudget}ms budget`,
46
- vars: {
47
- path: file,
48
- detail: `hook p95 ${rawP95}ms`,
49
- limit: `${rawBudget}ms`,
50
- },
51
- }));
52
- }
55
+ detail: `hook p95 ${rawP95}ms`,
56
+ limit: `${rawBudget}ms`,
57
+ },
58
+ }));
53
59
  }
54
- return findings;
55
- }));
60
+ });
56
61
  }
57
62
  // SPEC §4.14.2 core/guardrail-manifest: a missing manifest flags.
58
63
  export function resolveGuardrailManifest(decl, rule, ports) {
59
- const binding = bindRule(rule);
60
- return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
61
- const findings = [];
62
- for (const { file, markers } of await scopeMarkerTexts(ports, binding, ctx, findings)) {
63
- const manifest = markers.get("manifest") ?? "";
64
- if (manifest === "" || manifest === "missing") {
65
- findings.push(findingFor(binding, {
66
- path: file,
67
- message: "guardrail manifest missing",
68
- vars: { path: file, detail: "the guardrail manifest" },
69
- }));
70
- }
64
+ return markerGuard(decl, rule, ports, (guard, file, markers, findings) => {
65
+ const manifest = markers.get("manifest") ?? "";
66
+ if (manifest === "" || manifest === "missing") {
67
+ findings.push(findingFor(guard, {
68
+ path: file,
69
+ message: "guardrail manifest missing",
70
+ vars: { path: file, detail: "the guardrail manifest" },
71
+ }));
71
72
  }
72
- return findings;
73
- }));
73
+ });
74
74
  }
75
75
  // SPEC §4.14.2 core/hooks-active: hooks not installed flags.
76
76
  export function resolveHooksActive(decl, rule, ports) {
77
- const binding = bindRule(rule);
78
- return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
79
- const findings = [];
80
- for (const { file, markers } of await scopeMarkerTexts(ports, binding, ctx, findings)) {
81
- const hooks = markers.get("hooks") ?? "";
82
- if (hooks === "" || hooks === "not installed") {
83
- findings.push(findingFor(binding, {
84
- path: file,
85
- message: "git hooks not installed",
86
- vars: { path: file, detail: "the git hooks" },
87
- }));
88
- }
89
- }
90
- return findings;
91
- }));
92
- }
93
- // SPEC §4.14.2 core/engine-version-match: the declared version must equal
94
- // the installed engine; a pack ahead of the engine flags.
95
- export function resolveEngineVersionMatch(decl, rule, ports) {
96
- const binding = bindRule(rule);
97
- return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
98
- const findings = [];
99
- for (const file of selectFiles(ctx.scope.files, binding.scopeGlobs)) {
100
- if (file !== "engine.json" && !file.endsWith("/engine.json")) {
101
- continue;
102
- }
103
- const text = await loadScopeText(ports, binding, file, findings);
104
- if (text === undefined) {
105
- continue;
106
- }
107
- const parsed = parseJson(text);
108
- if (!parsed.ok) {
109
- findings.push(readErrorFinding(binding, file, parsed.error, "parse"));
110
- continue;
111
- }
112
- const record = (typeof parsed.value === "object" && parsed.value !== null
113
- ? parsed.value
114
- : {});
115
- const engine = record.engine;
116
- const pack = record.pack;
117
- if (typeof engine !== "string" || typeof pack !== "string") {
118
- continue;
119
- }
120
- if (engine !== pack) {
121
- findings.push(findingFor(binding, {
122
- path: file,
123
- message: `pack version ${pack} does not match the engine ${engine}`,
124
- vars: { path: file, value: engine },
125
- }));
126
- }
77
+ return markerGuard(decl, rule, ports, (guard, file, markers, findings) => {
78
+ const hooks = markers.get("hooks") ?? "";
79
+ if (hooks === "" || hooks === "not installed") {
80
+ findings.push(findingFor(guard, {
81
+ path: file,
82
+ message: "git hooks not installed",
83
+ vars: { path: file, detail: "the git hooks" },
84
+ }));
127
85
  }
128
- return findings;
129
- }));
86
+ });
130
87
  }
131
88
  // SPEC §4.3.4 core/waivers-valid: a waiver past the pack's maxWaiverDays
132
89
  // (core: 90, §5.12) flags.
133
90
  export function resolveWaiversValid(decl, rule, ports) {
134
- const binding = bindRule(rule);
135
- return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
136
- const findings = [];
137
- for (const { file, markers } of await scopeMarkerTexts(ports, binding, ctx, findings)) {
138
- const days = parseMarkerInt(markers, "waiver-days");
139
- const max = parseMarkerInt(markers, "max") ?? 90;
140
- if (days !== undefined && days > max) {
141
- findings.push(findingFor(binding, {
142
- path: file,
143
- message: `waiver at ${String(days)} days is past the ${String(max)} day bound`,
144
- vars: { path: file, detail: `waiver at ${String(days)} days` },
145
- }));
146
- }
91
+ return markerGuard(decl, rule, ports, (guard, file, markers, findings) => {
92
+ const days = parseMarkerInt(markers, "waiver-days");
93
+ const max = parseMarkerInt(markers, "max") ?? 90;
94
+ if (days !== undefined && days > max) {
95
+ findings.push(findingFor(guard, {
96
+ path: file,
97
+ message: `waiver at ${String(days)} days is past the ${String(max)} day bound`,
98
+ vars: { path: file, detail: `waiver at ${String(days)} days` },
99
+ }));
147
100
  }
148
- return findings;
149
- }));
101
+ });
150
102
  }
151
103
  // SPEC §4.4.7 core/custom-pack-selftest: a changed custom pack whose
152
104
  // self-test did not pass flags.
153
105
  export function resolveCustomPackSelftest(decl, rule, ports) {
154
- const binding = bindRule(rule);
155
- return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
156
- const findings = [];
157
- for (const { file, markers } of await scopeMarkerTexts(ports, binding, ctx, findings)) {
158
- const changed = markers.get("changed") ?? "";
159
- const tested = markers.get("self-test") ?? "";
160
- if (changed === "yes" && tested !== "passed") {
161
- findings.push(findingFor(binding, {
162
- path: file,
163
- message: "changed custom pack with no passing self-test",
164
- vars: { path: file, detail: "the changed custom pack" },
165
- }));
166
- }
106
+ return markerGuard(decl, rule, ports, (guard, file, markers, findings) => {
107
+ const changed = markers.get("changed") ?? "";
108
+ const tested = markers.get("self-test") ?? "";
109
+ if (changed === "yes" && tested !== "passed") {
110
+ findings.push(findingFor(guard, {
111
+ path: file,
112
+ message: "changed custom pack with no passing self-test",
113
+ vars: { path: file, detail: "the changed custom pack" },
114
+ }));
167
115
  }
168
- return findings;
169
- }));
116
+ });
170
117
  }
@@ -1,4 +1,5 @@
1
1
  import type { Ports } from "../../../kernel/index.ts";
2
+ import { type Result } from "../../../kernel/index.ts";
2
3
  import type { CheckContext, Enforcer, Finding } from "../../engine/index.ts";
3
4
  import type { PackRule } from "../index.ts";
4
5
  import type { EnforcerDecl } from "../manifest.ts";
@@ -16,6 +17,7 @@ export declare function scopeMarkerTexts(ports: Ports, binding: RuleBinding, ctx
16
17
  readonly markers: Map<string, string>;
17
18
  }>>;
18
19
  export declare function baseEnforcer(rule: PackRule, binding: RuleBinding, decl: EnforcerDecl, check: (ctx: CheckContext) => Promise<Array<Finding>>): Enforcer;
20
+ export declare function checkEnforcer(decl: EnforcerDecl, rule: PackRule, check: (binding: RuleBinding, ctx: CheckContext) => Promise<Array<Finding>>): Result<Enforcer>;
19
21
  export declare function limitOf(binding: RuleBinding, name: string, fallback: number): number;
20
22
  export { isMarkerFile, parseMarkerInt, parseMarkers };
21
23
  export declare function isTsFile(path: string): boolean;
@@ -1,4 +1,5 @@
1
- import { readErrorFinding, readLossyText, selectFiles, tunableNumber, } from "./binding.js";
1
+ import { ok } from "../../../kernel/index.js";
2
+ import { bindRule, readErrorFinding, readLossyText, selectFiles, tunableNumber, } from "./binding.js";
2
3
  import { blankCode } from "./health-lex.js";
3
4
  import { extractLiteralSpecifiers } from "./health-lex-imports.js";
4
5
  import { isMarkerFile, parseMarkerInt, parseMarkers, } from "./health-markers.js";
@@ -155,6 +156,14 @@ export function baseEnforcer(rule, binding, decl, check) {
155
156
  check,
156
157
  };
157
158
  }
159
+ // SPEC §5.6: the resolver preamble shared by the marker-driven enforcers:
160
+ // one binding, one budget-carrying enforcer, one async check over it. The
161
+ // check receives the binding, so resolvers keep no preamble of their own
162
+ // and the shared run below the duplication floor stays in one place.
163
+ export function checkEnforcer(decl, rule, check) {
164
+ const binding = bindRule(rule);
165
+ return ok(baseEnforcer(rule, binding, decl, (ctx) => check(binding, ctx)));
166
+ }
158
167
  export function limitOf(binding, name, fallback) {
159
168
  return tunableNumber(binding, name) ?? fallback;
160
169
  }
@@ -1,5 +1,4 @@
1
- import type { Ports } from "../../../kernel/index.ts";
2
- import { type Result } from "../../../kernel/index.ts";
1
+ import type { Ports, Result } from "../../../kernel/index.ts";
3
2
  import type { Enforcer } from "../../engine/index.ts";
4
3
  import type { PackRule } from "../index.ts";
5
4
  import type { EnforcerDecl } from "../manifest.ts";
@@ -1,61 +1,76 @@
1
- import { ok } from "../../../kernel/index.js";
2
- import { bindRule, findingFor, selectFiles } from "./binding.js";
1
+ import { findingFor, selectFiles } from "./binding.js";
3
2
  import { blankCode } from "./health-lex.js";
4
- import { baseEnforcer, exportedNamesOf, fileImportsTarget, isEntryFile, isMarkerFile, isTsFile, loadScopeText, parseMarkerInt, parseMarkers, } from "./health-measures.js";
5
- // SPEC §5.6 core/dead-code: no unused exports or members. The pack
6
- // measure is an exported name in a non-entry file with no importer.
7
- export function resolveDeadCode(decl, rule, ports) {
8
- const binding = bindRule(rule);
9
- return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
10
- const findings = [];
11
- const files = selectFiles(ctx.scope.files, binding.scopeGlobs);
12
- const texts = new Map();
13
- for (const file of files) {
14
- const text = await loadScopeText(ports, binding, file, findings);
15
- if (text !== undefined) {
16
- texts.set(file, text);
17
- }
3
+ import { checkEnforcer, exportedNamesOf, fileImportsTarget, isEntryFile, isMarkerFile, isTsFile, loadScopeText, parseMarkerInt, parseMarkers, } from "./health-measures.js";
4
+ async function loadScopeTexts(ports, binding, files, findings) {
5
+ const texts = new Map();
6
+ for (const file of files) {
7
+ const text = await loadScopeText(ports, binding, file, findings);
8
+ if (text !== undefined) {
9
+ texts.set(file, text);
18
10
  }
19
- for (const file of files) {
20
- const text = texts.get(file);
21
- if (text === undefined) {
22
- continue;
23
- }
24
- if (isMarkerFile(file)) {
25
- const markers = parseMarkers(text);
26
- const importers = parseMarkerInt(markers, "importers") ?? 1;
27
- const exported = markers.get("exported") ?? "export";
28
- if (importers === 0) {
29
- findings.push(findingFor(binding, {
30
- path: file,
31
- line: 1,
32
- message: `unused export ${exported} in ${file}`,
33
- vars: { path: file, line: "1", value: exported },
34
- }));
35
- }
36
- continue;
37
- }
38
- if (!isTsFile(file) || isEntryFile(file)) {
39
- continue;
40
- }
41
- const exported = exportedNamesOf(blankCode(text));
42
- if (exported.length === 0) {
43
- continue;
44
- }
45
- const imported = [...texts.entries()].some(([other, otherText]) => other !== file &&
46
- isTsFile(other) &&
47
- fileImportsTarget(otherText, file));
48
- if (!imported) {
49
- for (const name of exported) {
50
- findings.push(findingFor(binding, {
51
- path: file,
52
- line: 1,
53
- message: `unused export ${name} in ${file}`,
54
- vars: { path: file, line: "1", value: name },
55
- }));
56
- }
57
- }
11
+ }
12
+ return texts;
13
+ }
14
+ function markerDeadCode(binding, file, text) {
15
+ const markers = parseMarkers(text);
16
+ const importers = parseMarkerInt(markers, "importers") ?? 1;
17
+ const exported = markers.get("exported") ?? "export";
18
+ if (importers !== 0) {
19
+ return [];
20
+ }
21
+ return [
22
+ findingFor(binding, {
23
+ path: file,
24
+ line: 1,
25
+ message: `unused export ${exported} in ${file}`,
26
+ vars: { path: file, line: "1", value: exported },
27
+ }),
28
+ ];
29
+ }
30
+ function fileIsImported(texts, file) {
31
+ for (const [other, otherText] of texts.entries()) {
32
+ if (other !== file &&
33
+ isTsFile(other) &&
34
+ fileImportsTarget(otherText, file)) {
35
+ return true;
58
36
  }
59
- return findings;
37
+ }
38
+ return false;
39
+ }
40
+ function tsDeadCode(binding, file, text, texts) {
41
+ if (!isTsFile(file) || isEntryFile(file)) {
42
+ return [];
43
+ }
44
+ const exported = exportedNamesOf(blankCode(text));
45
+ if (exported.length === 0 || fileIsImported(texts, file)) {
46
+ return [];
47
+ }
48
+ return exported.map((name) => findingFor(binding, {
49
+ path: file,
50
+ line: 1,
51
+ message: `unused export ${name} in ${file}`,
52
+ vars: { path: file, line: "1", value: name },
60
53
  }));
61
54
  }
55
+ async function deadCodeCheck(ports, binding, ctx) {
56
+ const findings = [];
57
+ const files = selectFiles(ctx.scope.files, binding.scopeGlobs);
58
+ const texts = await loadScopeTexts(ports, binding, files, findings);
59
+ for (const file of files) {
60
+ const text = texts.get(file);
61
+ if (text === undefined) {
62
+ continue;
63
+ }
64
+ if (isMarkerFile(file)) {
65
+ findings.push(...markerDeadCode(binding, file, text));
66
+ continue;
67
+ }
68
+ findings.push(...tsDeadCode(binding, file, text, texts));
69
+ }
70
+ return findings;
71
+ }
72
+ // SPEC §5.6 core/dead-code: no unused exports or members. The pack
73
+ // measure is an exported name in a non-entry file with no importer.
74
+ export function resolveDeadCode(decl, rule, ports) {
75
+ return checkEnforcer(decl, rule, (binding, ctx) => deadCodeCheck(ports, binding, ctx));
76
+ }
@@ -1,22 +1,6 @@
1
- import { type Ports } from "../../../kernel/index.ts";
2
- import type { CheckContext, Finding } from "../../engine/index.ts";
1
+ import { type Ports, type Result } from "../../../kernel/index.ts";
2
+ import type { Enforcer } from "../../engine/index.ts";
3
3
  import type { PackRule } from "../index.ts";
4
4
  import type { EnforcerDecl } from "../manifest.ts";
5
- export declare function resolveNoSecrets(decl: EnforcerDecl, rule: PackRule, ports: Ports): {
6
- ok: true;
7
- value: {
8
- id: string;
9
- kinds: readonly ("compact" | "post-tool" | "pre-tool" | "prompt" | "review" | "scheduled" | "startup")[];
10
- budgetMs: number;
11
- check: (ctx: CheckContext) => Promise<Array<Finding>>;
12
- };
13
- };
14
- export declare function resolveNoSecretReads(decl: EnforcerDecl, rule: PackRule, ports: Ports): {
15
- ok: true;
16
- value: {
17
- id: string;
18
- kinds: readonly ("compact" | "post-tool" | "pre-tool" | "prompt" | "review" | "scheduled" | "startup")[];
19
- budgetMs: number;
20
- check: (ctx: CheckContext) => Promise<Array<Finding>>;
21
- };
22
- };
5
+ export declare function resolveNoSecrets(decl: EnforcerDecl, rule: PackRule, ports: Ports): Result<Enforcer>;
6
+ export declare function resolveNoSecretReads(decl: EnforcerDecl, rule: PackRule, ports: Ports): Result<Enforcer>;
@@ -3,8 +3,9 @@
3
3
  // redactSecrets, skipping fixture globs; no-secret-reads blocks secret
4
4
  // paths themselves and `read:` lines naming one, plus secret reads inside
5
5
  // shell sources. Findings carry the rule id the self-test asserts.
6
- import { analyseShell, isFixturePath, isSecretReadPath, matchDangerousShell, redactSecrets, } from "../../../kernel/index.js";
6
+ import { analyseShell, isFixturePath, isSecretReadPath, matchDangerousShell, ok, redactSecrets, } from "../../../kernel/index.js";
7
7
  import { bindRule, findingFor, readErrorFinding, readLossyText, selectFiles, } from "./binding.js";
8
+ import { baseEnforcer } from "./health-measures.js";
8
9
  import { extractShellSources } from "./shell-analyse.js";
9
10
  function firstDiffLine(before, after) {
10
11
  const beforeLines = before.split("\n");
@@ -82,19 +83,11 @@ function scanToolInput(binding, scope) {
82
83
  }
83
84
  export function resolveNoSecrets(decl, rule, ports) {
84
85
  const binding = bindRule(rule);
85
- return {
86
- ok: true,
87
- value: {
88
- id: rule.id,
89
- kinds: binding.triggers,
90
- budgetMs: decl.budgetMs,
91
- check: async (ctx) => {
92
- const findings = await scanScopeFiles(ports, binding, ctx.scope);
93
- findings.push(...scanToolInput(binding, ctx.scope));
94
- return findings;
95
- },
96
- },
97
- };
86
+ return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
87
+ const findings = await scanScopeFiles(ports, binding, ctx.scope);
88
+ findings.push(...scanToolInput(binding, ctx.scope));
89
+ return findings;
90
+ }));
98
91
  }
99
92
  function readLineTarget(line) {
100
93
  const trimmed = line.trimStart();
@@ -121,59 +114,60 @@ function checkProposedSecretReads(binding, scope) {
121
114
  }
122
115
  return findings;
123
116
  }
117
+ function scanReadLineTargets(binding, file, text, findings) {
118
+ for (const line of text.split("\n")) {
119
+ const target = readLineTarget(line);
120
+ if (target !== undefined && isSecretReadPath(target)) {
121
+ findings.push(findingFor(binding, {
122
+ path: file,
123
+ message: `secret material must not reach the agent context: ${target}`,
124
+ vars: { path: file },
125
+ }));
126
+ }
127
+ }
128
+ }
129
+ function scanShellSecretReads(binding, file, text, findings) {
130
+ for (const source of extractShellSources(text, file)) {
131
+ const parsed = analyseShell(source);
132
+ if (!parsed.ok) {
133
+ continue;
134
+ }
135
+ for (const violation of matchDangerousShell(parsed.value)) {
136
+ if (violation.class !== "secret-read") {
137
+ continue;
138
+ }
139
+ findings.push(findingFor(binding, {
140
+ path: file,
141
+ message: `shell command reads secret material in ${file}: ${redactSecrets(violation.raw)}`,
142
+ vars: { path: file },
143
+ }));
144
+ }
145
+ }
146
+ }
147
+ async function scanFileSecretReads(ports, binding, file, findings) {
148
+ if (isSecretReadPath(file)) {
149
+ findings.push(findingFor(binding, {
150
+ path: file,
151
+ message: `secret material must not reach the agent context: ${file}`,
152
+ vars: { path: file },
153
+ }));
154
+ }
155
+ const loaded = await readLossyText(ports.fs, file);
156
+ if (!loaded.ok) {
157
+ findings.push(readErrorFinding(binding, file, loaded.error, "read"));
158
+ return;
159
+ }
160
+ scanReadLineTargets(binding, file, loaded.value.text, findings);
161
+ scanShellSecretReads(binding, file, loaded.value.text, findings);
162
+ }
124
163
  export function resolveNoSecretReads(decl, rule, ports) {
125
164
  const binding = bindRule(rule);
126
- return {
127
- ok: true,
128
- value: {
129
- id: rule.id,
130
- kinds: binding.triggers,
131
- budgetMs: decl.budgetMs,
132
- check: async (ctx) => {
133
- const findings = [];
134
- for (const file of selectFiles(ctx.scope.files, binding.scopeGlobs)) {
135
- if (isSecretReadPath(file)) {
136
- findings.push(findingFor(binding, {
137
- path: file,
138
- message: `secret material must not reach the agent context: ${file}`,
139
- vars: { path: file },
140
- }));
141
- }
142
- const loaded = await readLossyText(ports.fs, file);
143
- if (!loaded.ok) {
144
- findings.push(readErrorFinding(binding, file, loaded.error, "read"));
145
- continue;
146
- }
147
- for (const line of loaded.value.text.split("\n")) {
148
- const target = readLineTarget(line);
149
- if (target !== undefined && isSecretReadPath(target)) {
150
- findings.push(findingFor(binding, {
151
- path: file,
152
- message: `secret material must not reach the agent context: ${target}`,
153
- vars: { path: file },
154
- }));
155
- }
156
- }
157
- for (const source of extractShellSources(loaded.value.text, file)) {
158
- const parsed = analyseShell(source);
159
- if (!parsed.ok) {
160
- continue;
161
- }
162
- for (const violation of matchDangerousShell(parsed.value)) {
163
- if (violation.class !== "secret-read") {
164
- continue;
165
- }
166
- findings.push(findingFor(binding, {
167
- path: file,
168
- message: `shell command reads secret material in ${file}: ${redactSecrets(violation.raw)}`,
169
- vars: { path: file },
170
- }));
171
- }
172
- }
173
- }
174
- findings.push(...checkProposedSecretReads(binding, ctx.scope));
175
- return findings;
176
- },
177
- },
178
- };
165
+ return ok(baseEnforcer(rule, binding, decl, async (ctx) => {
166
+ const findings = [];
167
+ for (const file of selectFiles(ctx.scope.files, binding.scopeGlobs)) {
168
+ await scanFileSecretReads(ports, binding, file, findings);
169
+ }
170
+ findings.push(...checkProposedSecretReads(binding, ctx.scope));
171
+ return findings;
172
+ }));
179
173
  }