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.
- package/dist/builtin-packs/core/fixtures/conventional-commit/passing/basic/case.json +1 -1
- package/dist/builtin-packs/core/fixtures/conventional-commit/violating/basic/case.json +1 -1
- package/dist/builtin-packs/typescript/fixtures/lint/passing/exported/case.json +8 -0
- package/dist/builtin-packs/typescript/pack.json +1 -1
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +3 -1
- package/dist/cli/gate-scope.d.ts +1 -0
- package/dist/cli/gate-scope.js +11 -0
- package/dist/cli/git-hook.js +13 -8
- package/dist/cli/guardrail.d.ts +2 -0
- package/dist/cli/guardrail.js +11 -13
- package/dist/cli/init-plan.d.ts +7 -0
- package/dist/cli/init-plan.js +21 -2
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +8 -17
- package/dist/cli/plan-launcher.d.ts +1 -0
- package/dist/cli/plan-launcher.js +53 -3
- package/dist/cli/serve-run.js +1 -0
- package/dist/cli/setup-gitignore.d.ts +7 -0
- package/dist/cli/setup-gitignore.js +41 -0
- package/dist/cli/setup-handoff.d.ts +7 -0
- package/dist/cli/setup-handoff.js +44 -0
- package/dist/cli/setup-install.d.ts +11 -0
- package/dist/cli/setup-install.js +84 -0
- package/dist/cli/setup.d.ts +9 -0
- package/dist/cli/setup.js +143 -0
- package/dist/cli/task-advance.js +16 -16
- package/dist/features/adapters/claude-shaped.d.ts +1 -0
- package/dist/features/adapters/claude-shaped.js +29 -31
- package/dist/features/adapters/copilot.js +22 -29
- package/dist/features/audit/append.js +9 -4
- package/dist/features/audit/surfaces.js +15 -8
- package/dist/features/ci/protection.js +37 -13
- package/dist/features/deps/npm.js +39 -24
- package/dist/features/deps/registry.js +45 -34
- package/dist/features/deps/stable.js +52 -27
- package/dist/features/deps/upgrade.d.ts +1 -1
- package/dist/features/deps/upgrade.js +29 -19
- package/dist/features/engine/evaluate.js +6 -1
- package/dist/features/packs/enforcers/ci-guards.js +4 -1
- package/dist/features/packs/enforcers/code.js +2 -1
- package/dist/features/packs/enforcers/engine-version-guards.d.ts +6 -0
- package/dist/features/packs/enforcers/engine-version-guards.js +42 -0
- package/dist/features/packs/enforcers/external-bind.js +62 -28
- package/dist/features/packs/enforcers/external-sarif.js +30 -24
- package/dist/features/packs/enforcers/gate-guards.d.ts +0 -1
- package/dist/features/packs/enforcers/gate-guards.js +83 -136
- package/dist/features/packs/enforcers/health-measures.d.ts +2 -0
- package/dist/features/packs/enforcers/health-measures.js +10 -1
- package/dist/features/packs/enforcers/health-refs.d.ts +1 -2
- package/dist/features/packs/enforcers/health-refs.js +71 -56
- package/dist/features/packs/enforcers/secret-scan.d.ts +4 -20
- package/dist/features/packs/enforcers/secret-scan.js +61 -67
- package/dist/features/packs/enforcers/ts-checks.js +104 -58
- package/dist/features/packs/enforcers/ts-security-patterns.js +12 -8
- package/dist/features/packs/manifest-decl.d.ts +13 -0
- package/dist/features/packs/manifest-decl.js +121 -0
- package/dist/features/packs/manifest.d.ts +2 -12
- package/dist/features/packs/manifest.js +5 -90
- package/dist/features/packs/selftest.js +22 -5
- package/dist/features/remediation/adoption.js +40 -29
- package/dist/features/testing/scenarios.js +49 -27
- package/dist/features/testing/tap-parser.js +38 -26
- package/dist/features/workflow/phases.js +45 -42
- package/dist/features/workflow/review.js +17 -10
- package/dist/kernel/git.js +4 -1
- package/dist/kernel/pattern.js +120 -86
- package/dist/kernel/secret-paths.js +34 -20
- package/dist/kernel/shell-cmd.js +23 -6
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import { ok
|
|
2
|
-
import { bindRule, findingFor,
|
|
3
|
-
import { baseEnforcer,
|
|
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
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
limit: `${rawBudget}ms`,
|
|
50
|
-
},
|
|
51
|
-
}));
|
|
52
|
-
}
|
|
55
|
+
detail: `hook p95 ${rawP95}ms`,
|
|
56
|
+
limit: `${rawBudget}ms`,
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
53
59
|
}
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
}
|