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
|
@@ -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
|
+
}
|
package/dist/cli/task-advance.js
CHANGED
|
@@ -87,31 +87,31 @@ async function taskMutate(req, root, sub) {
|
|
|
87
87
|
}
|
|
88
88
|
return taskClose(req, root, id);
|
|
89
89
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
79
|
+
return `merge-base(${input.defaultBranch ?? "main"},HEAD)`;
|
|
56
80
|
}
|
|
57
|
-
return
|
|
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
|
|
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(
|
|
32
|
-
|
|
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
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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.
|