@peterxiaoyang/superspec 0.1.7 → 0.1.8

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.
@@ -0,0 +1,4 @@
1
+ import type { Reason } from "../util.ts";
2
+ export declare function hookManifestHash(repoRoot: string): string | null;
3
+ export declare function managedHooksManifestPresent(repoRoot: string): boolean;
4
+ export declare function hookInitReasons(repoRoot: string): Reason[];
@@ -0,0 +1,98 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { reason, sha256_file } from "../util.js";
4
+ import { HOOK_ADAPTER_VERSION } from "./types.js";
5
+ const HOOK_COMMAND = 'superspec-hook --change "$SUPERSPEC_CHANGE"';
6
+ const EXPECTED_HOOK_MATRIX = [
7
+ {
8
+ eventName: "PreToolUse",
9
+ matcher: "Bash|apply_patch|Edit|Write|mcp__.*",
10
+ timeout: 10,
11
+ statusMessage: "SuperSpec hook write policy",
12
+ },
13
+ {
14
+ eventName: "PostToolUse",
15
+ matcher: "Bash",
16
+ timeout: 30,
17
+ statusMessage: "SuperSpec hook runtime evidence",
18
+ },
19
+ {
20
+ eventName: "SubagentStart",
21
+ matcher: ".*",
22
+ timeout: 30,
23
+ statusMessage: "SuperSpec hook subagent start",
24
+ },
25
+ {
26
+ eventName: "SubagentStop",
27
+ matcher: ".*",
28
+ timeout: 30,
29
+ statusMessage: "SuperSpec hook subagent stop",
30
+ },
31
+ ];
32
+ function isRecord(value) {
33
+ return !!value && typeof value === "object" && !Array.isArray(value);
34
+ }
35
+ function hasExactKeys(value, keys) {
36
+ const actual = Object.keys(value).sort();
37
+ const expected = [...keys].sort();
38
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
39
+ }
40
+ function hookEntryMatches(entry, expected) {
41
+ if (entry.matcher !== expected.matcher)
42
+ return false;
43
+ if (!Array.isArray(entry.hooks) || entry.hooks.length !== 1)
44
+ return false;
45
+ const hook = entry.hooks[0];
46
+ if (!isRecord(hook))
47
+ return false;
48
+ if (!hasExactKeys(hook, ["type", "command", "timeout", "statusMessage"]))
49
+ return false;
50
+ return hook.type === "command"
51
+ && hook.command === HOOK_COMMAND
52
+ && hook.timeout === expected.timeout
53
+ && hook.statusMessage === expected.statusMessage;
54
+ }
55
+ function hookMatrixPresent(parsed) {
56
+ const hooks = parsed?.hooks;
57
+ if (!hooks || typeof hooks !== "object")
58
+ return false;
59
+ if (!isRecord(hooks))
60
+ return false;
61
+ if (!hasExactKeys(hooks, EXPECTED_HOOK_MATRIX.map((entry) => entry.eventName)))
62
+ return false;
63
+ return EXPECTED_HOOK_MATRIX.every((expected) => {
64
+ const entries = hooks[expected.eventName];
65
+ return Array.isArray(entries)
66
+ && entries.length === 1
67
+ && hookEntryMatches(entries[0], expected);
68
+ });
69
+ }
70
+ export function hookManifestHash(repoRoot) {
71
+ const hookPath = join(repoRoot, ".codex", "hooks.json");
72
+ if (!existsSync(hookPath) || !statSync(hookPath).isFile())
73
+ return null;
74
+ return sha256_file(hookPath);
75
+ }
76
+ export function managedHooksManifestPresent(repoRoot) {
77
+ const hookPath = join(repoRoot, ".codex", "hooks.json");
78
+ if (!existsSync(hookPath) || !statSync(hookPath).isFile())
79
+ return false;
80
+ try {
81
+ const parsed = JSON.parse(readFileSync(hookPath, "utf8"));
82
+ return parsed?.superspec?.managed === true
83
+ && parsed?.superspec?.adapter_version === HOOK_ADAPTER_VERSION
84
+ && parsed?.superspec?.strict_profile_default === "audit-only-until-r1-provenance-passes"
85
+ && hookMatrixPresent(parsed);
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ export function hookInitReasons(repoRoot) {
92
+ const hookPath = join(repoRoot, ".codex", "hooks.json");
93
+ if (!existsSync(hookPath))
94
+ return [];
95
+ if (managedHooksManifestPresent(repoRoot))
96
+ return [];
97
+ return [reason("hook_manifest_unmanaged", ".codex/hooks.json exists but is not the SuperSpec-managed v2 hook manifest; strict profile must downgrade")];
98
+ }
@@ -0,0 +1,41 @@
1
+ import type { JsonMap, Reason } from "../util.ts";
2
+ import type { HookEvent } from "./types.ts";
3
+ export type NormalizedHookEvent = {
4
+ event: HookEvent;
5
+ hook_event_id: string;
6
+ hook_event_name: string;
7
+ session_id: string;
8
+ cwd: string;
9
+ tool_name: string;
10
+ command: string;
11
+ };
12
+ export type WriteExtraction = {
13
+ target_paths: string[];
14
+ trust_root_text_matches: string[];
15
+ archive_command: boolean;
16
+ internal_hook_writer_command: boolean;
17
+ unsafe_lifecycle_termination_command: boolean;
18
+ unsupported_write_surface: boolean;
19
+ task_checkbox_completions: string[];
20
+ task_checkbox_reopens: string[];
21
+ shell_write_command: boolean;
22
+ reasons: Reason[];
23
+ };
24
+ export declare function readHookEventRef(eventRef: string): HookEvent;
25
+ export declare function normalizeHookEvent(event: HookEvent): NormalizedHookEvent;
26
+ export declare function cleanRelPath(pathValue: string): string;
27
+ export declare function pathsIntersect(a: string, b: string): boolean;
28
+ export declare function isSuperSpecTrustRootPath(pathValue: string, opts?: {
29
+ changeRootRel?: string;
30
+ }): boolean;
31
+ export declare function anyPathInScope(paths: string[], scope: string): boolean;
32
+ export declare function commandLooksLikeTestValidation(command: string, depth?: number): boolean;
33
+ export declare function extractWriteIntent(event: HookEvent, repoRoot: string): WriteExtraction;
34
+ export declare function eventContentHash(event: HookEvent): string;
35
+ export declare function nonce(): string;
36
+ export declare function relFromChangeOrRepo(repoRoot: string, changeRoot: string, pathValue: string): {
37
+ root: "repo" | "change";
38
+ path: string;
39
+ } | null;
40
+ export declare function pinnedFileRef(baseRoot: string, relPath: string): JsonMap | null;
41
+ export declare function renderReasons(reasons: Reason[]): string;