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
@@ -0,0 +1,143 @@
1
+ // SPEC §4.19.6: one-shot adoption. Flags mirror init (§4.19.2); the run
2
+ // is the init path (or regen over an existing config) plus install, hooks
3
+ // and verify, ending at the extension handoff (§4.20.1). Every step
4
+ // delegates to the existing init, guardrail and verify handlers; no step
5
+ // logic is duplicated here.
6
+ import { ENGINE_VERSION } from "../features/config/index.js";
7
+ import { err, ok } from "../kernel/index.js";
8
+ import { configureHooksPath, handleRegen, writeHookFiles, } from "./guardrail.js";
9
+ import { parseInitFlags } from "./init.js";
10
+ import { defaultHarnesses, initPreamble, runInitSequence, } from "./init-plan.js";
11
+ import { handleVerify } from "./inspect.js";
12
+ import { setupGitignoreStep } from "./setup-gitignore.js";
13
+ import { hasEffectivePacks, SETUP_EXTENSION_ID, SETUP_EXTENSION_INSTALL, setupHandoffLines, } from "./setup-handoff.js";
14
+ import { setupInstallHere, setupInstallLayout } from "./setup-install.js";
15
+ import { fail, MIN_NODE_MAJOR, nodeMajor, nodeTooOldMessage, printHuman, printJson, readStateText, usageError, } from "./shared.js";
16
+ export const SETUP_USAGE = "usage: codictator setup [--harness <id> ...] [--profile <app|library|cli|service>] [--pack <id> ...] [--tool <role>=<command>@<pin> ...] [--repair]";
17
+ // SPEC §4.19.6 step 1: the Node floor, the human gate and the repo root.
18
+ async function setupPreflight(req) {
19
+ const version = req.nodeVersion ?? process.version;
20
+ if (nodeMajor(version) < MIN_NODE_MAJOR) {
21
+ return fail("CDX-E002", nodeTooOldMessage(version), "Install a newer Node, then retry the tool call.", req.json);
22
+ }
23
+ return initPreamble(req);
24
+ }
25
+ // SPEC §4.19.6 step 2: the adoption path. An existing config without
26
+ // --repair re-renders from config; otherwise the init path runs.
27
+ async function setupRegenPath(req, steps) {
28
+ const regen = await handleRegen({ ...req, args: [] });
29
+ if (!regen.ok) {
30
+ return regen;
31
+ }
32
+ steps.push({ step: "init", ok: true, detail: { mode: "regen" } });
33
+ printHuman("setup: guardrails regenerated from the existing config", req.quiet);
34
+ return ok(0);
35
+ }
36
+ async function setupInitPath(req, root, flags, steps) {
37
+ const engineVersion = req.engineVersion ?? ENGINE_VERSION;
38
+ const sequenced = await runInitSequence(root, req, flags, defaultHarnesses(flags), engineVersion);
39
+ if (!sequenced.ok) {
40
+ return sequenced;
41
+ }
42
+ steps.push({
43
+ step: "init",
44
+ ok: true,
45
+ detail: {
46
+ mode: flags.repair ? "repair" : "fresh",
47
+ files: sequenced.value.written.map((entry) => entry.path),
48
+ },
49
+ });
50
+ printHuman("setup: guardrails written from the init path", req.quiet);
51
+ return ok(0);
52
+ }
53
+ async function setupAdopt(req, root, flags, steps) {
54
+ const existing = await readStateText(root, ".codictator/config.json");
55
+ if (existing !== undefined && !flags.repair) {
56
+ return setupRegenPath(req, steps);
57
+ }
58
+ return setupInitPath(req, root, flags, steps);
59
+ }
60
+ // SPEC §4.19.6 step 6: hooks via the install-hooks writers.
61
+ async function setupHooksStep(req, root, steps) {
62
+ const written = await writeHookFiles(root);
63
+ if (!written.ok) {
64
+ return fail("CDX-E903", written.error.message, written.error.remediation, req.json);
65
+ }
66
+ await configureHooksPath(root, req);
67
+ steps.push({ step: "hooks", ok: true });
68
+ printHuman("setup: hooks installed", req.quiet);
69
+ return ok(0);
70
+ }
71
+ // SPEC §4.19.6 step 7: the first evaluation over the adopted repo. A
72
+ // blocking verdict stops the run with verify's own mapping (exit 1).
73
+ async function setupVerifyStep(req, steps) {
74
+ const verdict = await handleVerify({ ...req, args: [] });
75
+ if (!verdict.ok || verdict.value !== 0) {
76
+ return verdict;
77
+ }
78
+ steps.push({ step: "verify", ok: true });
79
+ return ok(0);
80
+ }
81
+ // SPEC §4.19.6 step 8 + §4.20.1: the views connect once a config exists;
82
+ // the human installs the extension with the printed command.
83
+ function setupHandoff(req, root, steps, emptyPacks) {
84
+ steps.push({
85
+ step: "handoff",
86
+ ok: true,
87
+ detail: { extension: SETUP_EXTENSION_ID },
88
+ });
89
+ if (req.json) {
90
+ printJson({
91
+ setup: true,
92
+ root,
93
+ steps,
94
+ extension: { id: SETUP_EXTENSION_ID, install: SETUP_EXTENSION_INSTALL },
95
+ });
96
+ }
97
+ else {
98
+ for (const line of setupHandoffLines(emptyPacks)) {
99
+ printHuman(line, req.quiet);
100
+ }
101
+ }
102
+ return ok(0);
103
+ }
104
+ // SPEC §4.19.6: the eight adoption steps in order; a step failure stops
105
+ // the run with that step's §8.2 mapping.
106
+ export async function handleSetup(req) {
107
+ const flags = parseInitFlags(req.args);
108
+ if (flags === undefined) {
109
+ return usageError(SETUP_USAGE);
110
+ }
111
+ const preflight = await setupPreflight(req);
112
+ if (typeof preflight !== "string") {
113
+ return preflight;
114
+ }
115
+ const root = preflight;
116
+ const steps = [{ step: "preflight", ok: true }];
117
+ const adopted = await setupAdopt(req, root, flags, steps);
118
+ if (!adopted.ok) {
119
+ return adopted;
120
+ }
121
+ const ignored = await setupGitignoreStep(req, root, steps);
122
+ if (!ignored.ok) {
123
+ return ignored;
124
+ }
125
+ const layout = await setupInstallLayout(req, root, steps);
126
+ if (!layout.ok) {
127
+ return err(layout.error);
128
+ }
129
+ const installed = await setupInstallHere(req, root, layout.value, steps);
130
+ if (!installed.ok) {
131
+ return installed;
132
+ }
133
+ const hooked = await setupHooksStep(req, root, steps);
134
+ if (!hooked.ok) {
135
+ return hooked;
136
+ }
137
+ const verified = await setupVerifyStep(req, steps);
138
+ if (!verified.ok) {
139
+ return verified;
140
+ }
141
+ const emptyPacks = !(await hasEffectivePacks(root, flags));
142
+ return setupHandoff(req, root, steps, emptyPacks);
143
+ }
@@ -87,31 +87,31 @@ async function taskMutate(req, root, sub) {
87
87
  }
88
88
  return taskClose(req, root, id);
89
89
  }
90
- // SPEC §4.19.2 + §4.12: task lifecycle reads and writes; only ack is
91
- // human-only, and only it clears an escalation.
92
- export async function handleTask(req) {
93
- const root = await needTaskRoot(req);
94
- if (typeof root !== "string") {
95
- return root;
96
- }
97
- const sub = req.args[0] ?? "";
98
- if (sub === "new") {
99
- return taskNew(req, root);
100
- }
101
- if (sub === "show") {
102
- return taskShow(req, root);
103
- }
90
+ const SINGLE_SUBCOMMANDS = {
91
+ new: (req, root) => taskNew(req, root),
92
+ show: (req, root) => taskShow(req, root),
93
+ checkpoint: (req, root) => taskCheckpoint(req, root),
94
+ };
95
+ async function runTaskSub(req, root, sub) {
104
96
  if (sub === "phase" && req.args[1] === "next") {
105
97
  return taskPhaseNext(req, root);
106
98
  }
107
- if (sub === "checkpoint") {
108
- return taskCheckpoint(req, root);
99
+ const single = SINGLE_SUBCOMMANDS[sub];
100
+ if (single !== undefined) {
101
+ return single(req, root);
109
102
  }
110
103
  if (sub === "ack" || sub === "amend" || sub === "close") {
111
104
  return taskMutate(req, root, sub);
112
105
  }
113
106
  return usageError("usage: codictator task <new|show|amend|phase next|checkpoint|close|ack> ...");
114
107
  }
108
+ export async function handleTask(req) {
109
+ const root = await needTaskRoot(req);
110
+ if (typeof root !== "string") {
111
+ return root;
112
+ }
113
+ return runTaskSub(req, root, req.args[0] ?? "");
114
+ }
115
115
  async function taskShow(req, root) {
116
116
  const ids = await listTaskIds(root);
117
117
  if (req.args.length === 1) {
@@ -1,3 +1,4 @@
1
1
  import type { Result } from "../../kernel/index.ts";
2
2
  import type { HookEvent, ToolKind } from "./contract.ts";
3
+ export declare function readString(record: Record<string, unknown>, keys: ReadonlyArray<string>): string | undefined;
3
4
  export declare function parseClaudeShapedEvent(harness: string, event: string, raw: unknown, table: Readonly<Record<string, ToolKind>>): Result<HookEvent>;
@@ -4,10 +4,9 @@
4
4
  // leaks (§9.2).
5
5
  import { err, makeError, ok } from "../../kernel/index.js";
6
6
  import { normalizeToolName } from "./contract.js";
7
- function isRecord(value) {
8
- return typeof value === "object" && value !== null && !Array.isArray(value);
9
- }
10
- function readString(record, keys) {
7
+ // SPEC §4.5.3: first non-empty string under any of the keys. Shared with
8
+ // the sibling adapters, so the field readers never drift apart.
9
+ export function readString(record, keys) {
11
10
  for (const key of keys) {
12
11
  const value = record[key];
13
12
  if (typeof value === "string" && value.length > 0) {
@@ -16,6 +15,9 @@ function readString(record, keys) {
16
15
  }
17
16
  return undefined;
18
17
  }
18
+ function isRecord(value) {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
19
21
  function readToolInput(raw) {
20
22
  for (const key of ["tool_input", "toolInput", "input", "args"]) {
21
23
  const value = raw[key];
@@ -42,34 +44,30 @@ function collectPaths(input) {
42
44
  }
43
45
  return [...found].sort();
44
46
  }
47
+ // SPEC §4.5.3: event names map to triggers through one table, so adding a
48
+ // harness alias is a row, not a branch.
49
+ const CLAUDE_EVENT_TABLE = {
50
+ PreToolUse: "pre-tool",
51
+ "tool.pre.write": "pre-tool",
52
+ "tool.pre.shell": "pre-tool",
53
+ "tool.pre.read": "pre-tool",
54
+ "tool.pre.search": "pre-tool",
55
+ "tool.pre.other": "pre-tool",
56
+ PostToolUse: "post-tool",
57
+ "tool.post.write": "post-tool",
58
+ Stop: "post-tool",
59
+ stop: "post-tool",
60
+ SessionStart: "startup",
61
+ "session.start": "startup",
62
+ SubagentStart: "startup",
63
+ "subagent.start": "startup",
64
+ UserPromptSubmit: "prompt",
65
+ "prompt.submit": "prompt",
66
+ PostCompact: "compact",
67
+ "compact.post": "compact",
68
+ };
45
69
  function mapClaudeEvent(event) {
46
- switch (event) {
47
- case "PreToolUse":
48
- case "tool.pre.write":
49
- case "tool.pre.shell":
50
- case "tool.pre.read":
51
- case "tool.pre.search":
52
- case "tool.pre.other":
53
- return "pre-tool";
54
- case "PostToolUse":
55
- case "tool.post.write":
56
- case "Stop":
57
- case "stop":
58
- return "post-tool";
59
- case "SessionStart":
60
- case "session.start":
61
- case "SubagentStart":
62
- case "subagent.start":
63
- return "startup";
64
- case "UserPromptSubmit":
65
- case "prompt.submit":
66
- return "prompt";
67
- case "PostCompact":
68
- case "compact.post":
69
- return "compact";
70
- default:
71
- return undefined;
72
- }
70
+ return CLAUDE_EVENT_TABLE[event];
73
71
  }
74
72
  function blockError(harness, detail) {
75
73
  return makeError("CDX-E303", `unparseable ${harness} hook payload: ${detail}`, "Check the harness hook configuration, then retry the tool call.");
@@ -1,4 +1,9 @@
1
+ // SPEC §4.5.4: the GitHub Copilot adapter (CLI, cloud agent, VS Code
2
+ // agent). Parse, reply and staticBlock are pure; hookIoVerified stays false
3
+ // until a first-party capture lands (§4.5.7), and the hooks-file schema
4
+ // below follows the SPEC row verbatim for the same reason.
1
5
  import { err, makeError, ok, stableStringify } from "../../kernel/index.js";
6
+ import { readString } from "./claude-shaped.js";
2
7
  import { normalizeToolName } from "./contract.js";
3
8
  import { COPILOT_CAPABILITIES } from "./copilot-capabilities.js";
4
9
  // SPEC §4.5.4 with §4.5.5: hook entries carry timeoutSec; the runtime budget
@@ -31,40 +36,28 @@ const COPILOT_TOOL_TABLE = {
31
36
  export function normalizeCopilotToolName(name) {
32
37
  return normalizeToolName(COPILOT_TOOL_TABLE, name);
33
38
  }
39
+ // SPEC §4.5.4: event names map to triggers through one table, so adding a
40
+ // harness alias is a row, not a branch.
41
+ const COPILOT_EVENT_TABLE = {
42
+ preToolUse: "pre-tool",
43
+ "tool.pre.write": "pre-tool",
44
+ "tool.pre.shell": "pre-tool",
45
+ "tool.pre.read": "pre-tool",
46
+ "tool.pre.search": "pre-tool",
47
+ "tool.pre.other": "pre-tool",
48
+ postToolUse: "post-tool",
49
+ "tool.post.write": "post-tool",
50
+ sessionStart: "startup",
51
+ "session.start": "startup",
52
+ userPromptSubmitted: "prompt",
53
+ "prompt.submit": "prompt",
54
+ };
34
55
  function mapCopilotEvent(event) {
35
- switch (event) {
36
- case "preToolUse":
37
- case "tool.pre.write":
38
- case "tool.pre.shell":
39
- case "tool.pre.read":
40
- case "tool.pre.search":
41
- case "tool.pre.other":
42
- return "pre-tool";
43
- case "postToolUse":
44
- case "tool.post.write":
45
- return "post-tool";
46
- case "sessionStart":
47
- case "session.start":
48
- return "startup";
49
- case "userPromptSubmitted":
50
- case "prompt.submit":
51
- return "prompt";
52
- default:
53
- return undefined;
54
- }
56
+ return COPILOT_EVENT_TABLE[event];
55
57
  }
56
58
  function isRecord(value) {
57
59
  return typeof value === "object" && value !== null && !Array.isArray(value);
58
60
  }
59
- function readString(record, keys) {
60
- for (const key of keys) {
61
- const value = record[key];
62
- if (typeof value === "string" && value.length > 0) {
63
- return value;
64
- }
65
- }
66
- return undefined;
67
- }
68
61
  function blockError(detail) {
69
62
  return makeError("CDX-E303", `unparseable copilot hook payload: ${detail}`, "Check the harness hook configuration, then retry the tool call.");
70
63
  }
@@ -87,18 +87,23 @@ function fitLine(event, now, id) {
87
87
  return err(e801("append event", "a single event without a message exceeds 64 KB"));
88
88
  }
89
89
  const empty = stableLineStringify(withOptionals(base, event, ""));
90
- let allowance = MAX_LINE_BYTES - byteLength(empty) - 2;
90
+ const allowance = MAX_LINE_BYTES - byteLength(empty) - 2;
91
+ return truncateToFit(base, event, message, allowance);
92
+ }
93
+ // Halve the budget until the marked message fits, else fail closed.
94
+ function truncateToFit(base, event, message, allowance) {
95
+ let budget = allowance;
91
96
  for (let round = 0; round < 32; round += 1) {
92
- if (allowance < 0) {
97
+ if (budget < 0) {
93
98
  return err(e801("append event", "the message cannot be truncated to fit 64 KB"));
94
99
  }
95
- const kept = truncateUtf8(message, allowance);
100
+ const kept = truncateUtf8(message, budget);
96
101
  const omitted = byteLength(message) - byteLength(kept);
97
102
  const candidate = stableLineStringify(withOptionals(base, event, `${kept}[truncated ${omitted} bytes]`));
98
103
  if (byteLength(candidate) <= MAX_LINE_BYTES) {
99
104
  return ok(candidate);
100
105
  }
101
- allowance = Math.floor(allowance / 2);
106
+ budget = Math.floor(budget / 2);
102
107
  }
103
108
  return err(e801("append event", "the message cannot be truncated to fit 64 KB"));
104
109
  }
@@ -27,6 +27,20 @@ function helpersLine(helpers) {
27
27
  });
28
28
  return `helpers: ${parts.join(", ")}`;
29
29
  }
30
+ // SPEC §4.18.3: trailing status rows with their fallbacks, so the screen
31
+ // builder stays a straight-line assembly.
32
+ function tailStatusLines(input) {
33
+ return [
34
+ `task: ${input.task ?? "none active"}`,
35
+ `waivers: ${input.waivers ?? "none in force"}`,
36
+ `ratchet: ${input.ratchet ?? "no baseline history"}`,
37
+ `deps: ${input.deps ?? "freshness unknown"}`,
38
+ `integrity: ${input.integrity ?? "unverified"}`,
39
+ `escalations: ${input.escalations ?? "none open"}`,
40
+ `remediation: ${input.remediation ?? "no plan"}`,
41
+ `events: ${input.events} observed, ${input.corrupt} corrupt${input.gap ? "; gap in coverage, log was deleted mid-session" : ""}`,
42
+ ];
43
+ }
30
44
  // SPEC §4.18.3: one screen with engine, packs, harness states, helper
31
45
  // tools, task, waivers, ratchet, deps, integrity, escalations and
32
46
  // remediation progress.
@@ -43,14 +57,7 @@ export function buildStatusText(input) {
43
57
  lines.push(`- ${harness.harness}: ${harness.state} (${harness.layers})`);
44
58
  }
45
59
  lines.push(helpersLine(input.helpers));
46
- lines.push(`task: ${input.task ?? "none active"}`);
47
- lines.push(`waivers: ${input.waivers ?? "none in force"}`);
48
- lines.push(`ratchet: ${input.ratchet ?? "no baseline history"}`);
49
- lines.push(`deps: ${input.deps ?? "freshness unknown"}`);
50
- lines.push(`integrity: ${input.integrity ?? "unverified"}`);
51
- lines.push(`escalations: ${input.escalations ?? "none open"}`);
52
- lines.push(`remediation: ${input.remediation ?? "no plan"}`);
53
- lines.push(`events: ${input.events} observed, ${input.corrupt} corrupt${input.gap ? "; gap in coverage, log was deleted mid-session" : ""}`);
60
+ lines.push(...tailStatusLines(input));
54
61
  return lines.join("\n");
55
62
  }
56
63
  // SPEC §4.18.3: the same status as data for the extension (--json).
@@ -13,25 +13,46 @@ function block(ruleId, message, remediation) {
13
13
  }
14
14
  // SPEC §4.15.2: reported not required → block; unreadable → advise (PRD
15
15
  // §11.1). The same read feeds core/branch-protection below.
16
- export function evaluateProtection(read) {
17
- if (read === undefined || !read.readable) {
18
- return [
19
- advise("core/ci-required-check", "cannot confirm the verify check is required: branch protection is unreadable", "Grant the workflow read access to rulesets/branch protection, or confirm the required check by hand (EC-1, EC-6)."),
20
- advise("core/branch-protection", "cannot confirm force-push, deletion and squash settings: branch protection is unreadable", "Grant the workflow read access to the repo settings and rulesets, or confirm them by hand (§4.15.2)."),
21
- ];
22
- }
23
- const findings = [];
16
+ function unreadableFindings() {
17
+ return [
18
+ advise("core/ci-required-check", "cannot confirm the verify check is required: branch protection is unreadable", "Grant the workflow read access to rulesets/branch protection, or confirm the required check by hand (EC-1, EC-6)."),
19
+ advise("core/branch-protection", "cannot confirm force-push, deletion and squash settings: branch protection is unreadable", "Grant the workflow read access to the repo settings and rulesets, or confirm them by hand (§4.15.2)."),
20
+ ];
21
+ }
22
+ function requiredCheckFinding(read) {
24
23
  if (read.verifyRequired !== true) {
25
- findings.push(block("core/ci-required-check", "the verify check is not a required check on the default branch", "Add verify to the required checks of the branch ruleset (§4.15 EC-7)."));
24
+ return block("core/ci-required-check", "the verify check is not a required check on the default branch", "Add verify to the required checks of the branch ruleset (§4.15 EC-7).");
26
25
  }
26
+ return undefined;
27
+ }
28
+ function squashFinding(read) {
27
29
  const squashExcluded = read.allowSquashMerge === false ||
28
30
  (read.rulesetMergeMethods !== undefined &&
29
31
  !read.rulesetMergeMethods.includes("squash"));
30
32
  if (!squashExcluded) {
31
- findings.push(block("core/branch-protection", "squash merging is allowed and no ruleset excludes it", "Set allow_squash_merge to false and exclude squash from the ruleset allowed_merge_methods (§4.9.3, EC-11, S33)."));
33
+ return block("core/branch-protection", "squash merging is allowed and no ruleset excludes it", "Set allow_squash_merge to false and exclude squash from the ruleset allowed_merge_methods (§4.9.3, EC-11, S33).");
32
34
  }
35
+ return undefined;
36
+ }
37
+ function hardeningFinding(read) {
33
38
  if (read.forcePushBlocked !== true || read.deletionBlocked !== true) {
34
- findings.push(block("core/branch-protection", "force-push or deletion is not blocked on the default branch", "Block force-pushes and deletions on the default branch in settings and rulesets (§4.15.2)."));
39
+ return block("core/branch-protection", "force-push or deletion is not blocked on the default branch", "Block force-pushes and deletions on the default branch in settings and rulesets (§4.15.2).");
40
+ }
41
+ return undefined;
42
+ }
43
+ export function evaluateProtection(read) {
44
+ if (read === undefined || !read.readable) {
45
+ return unreadableFindings();
46
+ }
47
+ const findings = [];
48
+ for (const maybe of [
49
+ requiredCheckFinding(read),
50
+ squashFinding(read),
51
+ hardeningFinding(read),
52
+ ]) {
53
+ if (maybe !== undefined) {
54
+ findings.push(maybe);
55
+ }
35
56
  }
36
57
  return findings;
37
58
  }
@@ -51,10 +72,13 @@ export function resolveBaseRef(input) {
51
72
  input.before !== zero) {
52
73
  return ok(input.before);
53
74
  }
75
+ return ok(baseFromDefault(input));
76
+ }
77
+ function baseFromDefault(input) {
54
78
  if (input.eventName === "push") {
55
- return ok(`merge-base(${input.defaultBranch ?? "main"},HEAD)`);
79
+ return `merge-base(${input.defaultBranch ?? "main"},HEAD)`;
56
80
  }
57
- return ok(input.pullBaseSha ?? `merge-base(${input.defaultBranch ?? "main"},HEAD)`);
81
+ return (input.pullBaseSha ?? `merge-base(${input.defaultBranch ?? "main"},HEAD)`);
58
82
  }
59
83
  function usesRef(uses) {
60
84
  const at = uses.indexOf("@");
@@ -17,10 +17,45 @@ function stripWorkspacePrefix(range) {
17
17
  }
18
18
  return undefined;
19
19
  }
20
+ function classifyWorkspaceRange(workspace) {
21
+ return EXACT_PIN.test(workspace) ? "workspace-exact" : "workspace-range";
22
+ }
23
+ function classifyAliasRange(range) {
24
+ const at = range.lastIndexOf("@");
25
+ const version = at < 4 ? "" : range.slice(at + 1);
26
+ return EXACT_PIN.test(version) ? "alias-exact" : "alias-range";
27
+ }
28
+ function isGitRange(range) {
29
+ return (range.startsWith("git+") ||
30
+ range.startsWith("git://") ||
31
+ range.startsWith("github:"));
32
+ }
33
+ function classifyGitRange(range) {
34
+ const hash = range.split("#")[1] ?? "";
35
+ return FULL_SHA.test(hash) ? "git-sha" : "git-unpinned";
36
+ }
37
+ function isTagName(range) {
38
+ return (range === "*" ||
39
+ range === "latest" ||
40
+ range === "" ||
41
+ /^[a-zA-Z][0-9A-Za-z.-]*$/.test(range));
42
+ }
43
+ function classifyBareRange(range) {
44
+ if (isGitRange(range)) {
45
+ return classifyGitRange(range);
46
+ }
47
+ if (range.startsWith("http://") || range.startsWith("https://")) {
48
+ return "tarball-url";
49
+ }
50
+ if (EXACT_PIN.test(range)) {
51
+ return "exact";
52
+ }
53
+ return isTagName(range) ? "tag" : "range";
54
+ }
20
55
  export function classifyNpmRange(range) {
21
56
  const workspace = stripWorkspacePrefix(range);
22
57
  if (workspace !== undefined) {
23
- return EXACT_PIN.test(workspace) ? "workspace-exact" : "workspace-range";
58
+ return classifyWorkspaceRange(workspace);
24
59
  }
25
60
  if (range.startsWith("file:")) {
26
61
  return "file";
@@ -28,30 +63,10 @@ export function classifyNpmRange(range) {
28
63
  if (range.startsWith("link:")) {
29
64
  return "link";
30
65
  }
31
- if (range.startsWith("npm:")) {
32
- const at = range.lastIndexOf("@");
33
- const version = at < 4 ? "" : range.slice(at + 1);
34
- return EXACT_PIN.test(version) ? "alias-exact" : "alias-range";
35
- }
36
- if (range.startsWith("git+") ||
37
- range.startsWith("git://") ||
38
- range.startsWith("github:")) {
39
- const hash = range.split("#")[1] ?? "";
40
- return FULL_SHA.test(hash) ? "git-sha" : "git-unpinned";
41
- }
42
- if (range.startsWith("http://") || range.startsWith("https://")) {
43
- return "tarball-url";
44
- }
45
- if (EXACT_PIN.test(range)) {
46
- return "exact";
47
- }
48
- if (range === "*" ||
49
- range === "latest" ||
50
- range === "" ||
51
- /^[a-zA-Z][0-9A-Za-z.-]*$/.test(range)) {
52
- return "tag";
66
+ if (range.startsWith(`${NPM_ECOSYSTEM}:`)) {
67
+ return classifyAliasRange(range);
53
68
  }
54
- return "range";
69
+ return classifyBareRange(range);
55
70
  }
56
71
  // SPEC §4.8.5: ranges, tags, *, latest and unpinned git/alias/workspace
57
72
  // forms block core/dep-exact-pin. In-repo file:/link: forms, full-SHA git
@@ -9,50 +9,61 @@ export const NPM_REGISTRY_BASE_URL = "https://registry.npmjs.org";
9
9
  export const REGISTRY_TIMEOUT_MS = 30_000;
10
10
  export const SNAPSHOT_MAX_AGE_MS = 24 * 86_400_000;
11
11
  export const SNAPSHOT_CACHE_PATH = ".codictator/cache/registry/npm.json";
12
- function narrowPackument(name, value) {
13
- const empty = {
14
- name,
15
- unpublished: true,
16
- times: {},
17
- versions: {},
18
- };
19
- if (!isJsonRecord(value)) {
20
- return empty;
21
- }
12
+ function isPublishTime(version, published) {
13
+ return (version !== "created" &&
14
+ version !== "modified" &&
15
+ typeof published === "string");
16
+ }
17
+ function narrowTimeTable(value) {
22
18
  const times = {};
23
19
  const timeTable = recordField(value, "time");
24
- if (timeTable !== undefined) {
25
- for (const [version, published] of Object.entries(timeTable)) {
26
- if (version !== "created" &&
27
- version !== "modified" &&
28
- typeof published === "string") {
29
- times[version] = published;
30
- }
20
+ if (timeTable === undefined) {
21
+ return times;
22
+ }
23
+ for (const [version, published] of Object.entries(timeTable)) {
24
+ if (isPublishTime(version, published)) {
25
+ times[version] = published;
31
26
  }
32
27
  }
28
+ return times;
29
+ }
30
+ function narrowVersionDetail(detail) {
31
+ if (!isJsonRecord(detail)) {
32
+ return undefined;
33
+ }
34
+ const enginesTable = recordField(detail, "engines");
35
+ return {
36
+ deprecated: typeof field(detail, "deprecated") === "string" ||
37
+ field(detail, "deprecated") === true,
38
+ license: stringField(detail, "license"),
39
+ engines: enginesTable === undefined
40
+ ? undefined
41
+ : stringField(enginesTable, "node"),
42
+ };
43
+ }
44
+ function narrowVersionTable(value) {
33
45
  const versions = {};
34
46
  const versionTable = recordField(value, "versions");
35
- if (versionTable !== undefined) {
36
- for (const [version, detail] of Object.entries(versionTable)) {
37
- if (!isJsonRecord(detail)) {
38
- continue;
39
- }
40
- const enginesTable = recordField(detail, "engines");
41
- const engines = enginesTable === undefined
42
- ? undefined
43
- : stringField(enginesTable, "node");
44
- versions[version] = {
45
- deprecated: typeof field(detail, "deprecated") === "string" ||
46
- field(detail, "deprecated") === true,
47
- license: stringField(detail, "license"),
48
- engines,
49
- };
47
+ if (versionTable === undefined) {
48
+ return versions;
49
+ }
50
+ for (const [version, detail] of Object.entries(versionTable)) {
51
+ const narrowed = narrowVersionDetail(detail);
52
+ if (narrowed !== undefined) {
53
+ versions[version] = narrowed;
50
54
  }
51
55
  }
56
+ return versions;
57
+ }
58
+ function narrowPackument(name, value) {
59
+ if (!isJsonRecord(value)) {
60
+ return { name, unpublished: true, times: {}, versions: {} };
61
+ }
62
+ const versions = narrowVersionTable(value);
52
63
  if (Object.keys(versions).length === 0) {
53
- return empty;
64
+ return { name, unpublished: true, times: {}, versions: {} };
54
65
  }
55
- return { name, unpublished: false, times, versions };
66
+ return { name, unpublished: false, times: narrowTimeTable(value), versions };
56
67
  }
57
68
  // SPEC §4.8.2: the full document (the abbreviated form lacks `time`).
58
69
  // A 404 is unpublished (EC-4), not a transport failure.