@cosmicdrift/kumiko-guards 0.362.0 → 0.364.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.
@@ -21,6 +21,7 @@
21
21
  * bun guard-upgrade-state.ts
22
22
  */
23
23
 
24
+ import { createHash } from "node:crypto";
24
25
  import { existsSync, readFileSync } from "node:fs";
25
26
  import { join } from "node:path";
26
27
  import {
@@ -60,45 +61,69 @@ export function resolveInstalledVersion(json: UpgradeJson): string {
60
61
  return json.installedVersion ?? json.currentVersion;
61
62
  }
62
63
 
64
+ function isRecord(value: unknown): value is Record<string, unknown> {
65
+ return typeof value === "object" && value !== null;
66
+ }
67
+
63
68
  export function isPendingEntry(value: unknown): value is PendingEntry {
64
- if (!value || typeof value !== "object") return false;
65
- const v = value as Record<string, unknown>;
69
+ if (!isRecord(value)) return false;
66
70
  return (
67
- typeof v["version"] === "string" &&
68
- typeof v["type"] === "string" &&
69
- typeof v["title"] === "string"
71
+ typeof value["version"] === "string" &&
72
+ typeof value["type"] === "string" &&
73
+ typeof value["title"] === "string"
70
74
  );
71
75
  }
72
76
 
73
77
  export function isUpgradeJson(value: unknown): value is UpgradeJson {
74
- if (!value || typeof value !== "object") return false;
75
- const v = value as Record<string, unknown>;
76
- const installed = v["installedVersion"];
78
+ if (!isRecord(value)) return false;
79
+ const installed = value["installedVersion"];
77
80
  return (
78
- typeof v["currentVersion"] === "string" &&
81
+ typeof value["currentVersion"] === "string" &&
79
82
  (installed === undefined || installed === null || typeof installed === "string") &&
80
- Array.isArray(v["pending"]) &&
81
- v["pending"].every(isPendingEntry)
83
+ Array.isArray(value["pending"]) &&
84
+ value["pending"].every(isPendingEntry)
82
85
  );
83
86
  }
84
87
 
85
- export function isPendingManualStep(value: unknown): value is PendingManualStep {
86
- if (!value || typeof value !== "object") return false;
87
- const v = value as Record<string, unknown>;
88
- return (
89
- typeof v["id"] === "string" &&
90
- v["id"].length > 0 &&
91
- typeof v["version"] === "string" &&
92
- typeof v["title"] === "string"
93
- );
88
+ // Same id scheme as manualEntryId in @cosmicdrift/kumiko-framework/upgrade-cli. Duplicated because
89
+ // this package must stay installable without the framework; a test pins both to the same value.
90
+ const MANUAL_ID_HASH_LENGTH = 8;
91
+
92
+ export function manualEntryId(version: string, title: string): string {
93
+ const hash = createHash("sha256").update(title).digest("hex").slice(0, MANUAL_ID_HASH_LENGTH);
94
+ return `${version}:${hash}`;
94
95
  }
95
96
 
96
- function readPendingManual(parsed: unknown): readonly PendingManualStep[] {
97
- const raw =
98
- parsed && typeof parsed === "object"
99
- ? (parsed as Record<string, unknown>)["pendingManual"]
100
- : undefined;
101
- return Array.isArray(raw) ? raw.filter(isPendingManualStep) : [];
97
+ /** Markers written before entries carried an `id` get the id the CLI derives for them. */
98
+ export function parsePendingManualStep(value: unknown): PendingManualStep | null {
99
+ if (!isRecord(value)) return null;
100
+ const { id, version, title } = value;
101
+ if (typeof version !== "string" || typeof title !== "string") return null;
102
+ return {
103
+ id: typeof id === "string" && id.length > 0 ? id : manualEntryId(version, title),
104
+ version,
105
+ title,
106
+ };
107
+ }
108
+
109
+ function readPendingManual(
110
+ parsed: unknown,
111
+ ): { readonly steps: readonly PendingManualStep[] } | { readonly error: string } {
112
+ const raw = isRecord(parsed) ? parsed["pendingManual"] : undefined;
113
+ if (raw === undefined) return { steps: [] };
114
+ if (!Array.isArray(raw))
115
+ return { error: `${MARKER_REL} has a "pendingManual" that is not an array` };
116
+ const steps: PendingManualStep[] = [];
117
+ for (const [index, entry] of raw.entries()) {
118
+ const step = parsePendingManualStep(entry);
119
+ if (step === null) {
120
+ return {
121
+ error: `${MARKER_REL} has a malformed pendingManual entry at index ${index} (needs string "version" and "title")`,
122
+ };
123
+ }
124
+ steps.push(step);
125
+ }
126
+ return { steps };
102
127
  }
103
128
 
104
129
  export function readMarker(root: string): UpgradeMarker | { error: string } {
@@ -114,16 +139,15 @@ export function readMarker(root: string): UpgradeMarker | { error: string } {
114
139
  } catch {
115
140
  return { error: `${MARKER_REL} is not valid JSON — broken marker file` };
116
141
  }
117
- const version =
118
- parsed && typeof parsed === "object"
119
- ? (parsed as Record<string, unknown>)["version"]
120
- : undefined;
142
+ const version = isRecord(parsed) ? parsed["version"] : undefined;
121
143
  if (typeof version !== "string" || !SEMVER_RE.test(version)) {
122
144
  return {
123
145
  error: `${MARKER_REL} is missing a valid "version" field (expected semver x.y.z[-pre][+build]) — broken marker file`,
124
146
  };
125
147
  }
126
- return { version, pendingManual: readPendingManual(parsed) };
148
+ const pendingManual = readPendingManual(parsed);
149
+ if ("error" in pendingManual) return pendingManual;
150
+ return { version, pendingManual: pendingManual.steps };
127
151
  }
128
152
 
129
153
  /** Pending changelog entries turned into guard violations — pure, testable without a subprocess. */
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { AGENT_DOCS_DIR, renderAgentDocs, writeAgentDocs } from "./_lib/agent-docs";
1
2
  export { gitEnv } from "./_lib/git-env";
2
3
  export {
3
4
  type AstGuard,
package/src/pre-push.sh CHANGED
@@ -18,6 +18,9 @@
18
18
  # sub-repo's own package.json `test` script — refuses the push if that
19
19
  # script is missing.
20
20
  #
21
+ # Before the checks below, the hook runs a gitleaks scan of origin/main..HEAD
22
+ # when the binary is installed.
23
+ #
21
24
  # Extension point: a tracked, executable scripts/pre-push-extra.sh runs
22
25
  # before the main check in every branch above, with KUMIKO_PUSH_REPO_ROOT
23
26
  # and KUMIKO_PUSH_PARENT_DIR available inline (never exported).
@@ -118,6 +121,22 @@ if [ -f "$REPO_ROOT/.kumiko/upgrade-state.json" ]; then
118
121
  fi
119
122
  fi
120
123
 
124
+ # Secret scan: runs before check-wt.sh/bun check so a leaked credential is
125
+ # caught in seconds, not after the long checks. A missing binary or a missing
126
+ # origin/main only warns, so a machine without gitleaks is not locked out.
127
+ # No --config: gitleaks reads a .gitleaks.toml in the repo root itself.
128
+ if ! command -v gitleaks >/dev/null 2>&1; then
129
+ echo "[pre-push] gitleaks not installed — secret scan skipped. Install: brew install gitleaks" >&2
130
+ elif ! git -C "$REPO_ROOT" rev-parse --verify --quiet origin/main >/dev/null 2>&1; then
131
+ echo "[pre-push] origin/main not found — secret scan skipped" >&2
132
+ else
133
+ echo "[pre-push] gitleaks git origin/main..HEAD"
134
+ if ! (cd "$REPO_ROOT" && gitleaks git --no-banner --redact --log-opts="origin/main..HEAD" .); then
135
+ echo "[pre-push] gitleaks found secrets — refusing push" >&2
136
+ exit 1
137
+ fi
138
+ fi
139
+
121
140
  # infra#559/#569: from a worktree, `--git-common-dir` resolves to the main
122
141
  # checkout's .git, so the branch below would scope `bun check` to the
123
142
  # canonical sibling path instead of this worktree — checking the wrong
@@ -7,6 +7,7 @@ import { check as realProviderIsolation } from "./check-real-provider-isolation"
7
7
  import { check as runtimeIsolation } from "./check-runtime-isolation";
8
8
  import { check as secretLiterals } from "./check-secret-literals";
9
9
  import { check as singleRuntimeInstance } from "./check-single-runtime-instance";
10
+ import { check as agentDocsDrift } from "./guard-agent-docs-drift";
10
11
  import { check as featureIntegrationTests } from "./guard-feature-integration-tests";
11
12
  import { check as noDirectProcessEnv } from "./guard-no-direct-process-env";
12
13
  import { check as primitivesDiscipline } from "./guard-primitives-discipline";
@@ -29,6 +30,7 @@ export const REPO_CHECKS = [
29
30
  upgradeState,
30
31
  realProviderIsolation,
31
32
  singleRuntimeInstance,
33
+ agentDocsDrift,
32
34
  ];
33
35
 
34
36
  export const REPO_CHECK_FLAGS: readonly string[] = ["--write-baseline"];