@envseal/core 0.1.6 → 0.1.7

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/paths.d.ts CHANGED
@@ -14,4 +14,13 @@ export declare function loadOrCreateSalt(paths: ProjectPaths): Buffer;
14
14
  export declare const HOOK_HEARTBEAT_FILE = "hook-heartbeat";
15
15
  /** ISO timestamp of the hook's last observed run for a project, or null. */
16
16
  export declare function readHookHeartbeat(root: string): string | null;
17
+ export declare const HOOK_DECISIONS_FILE = "hook-decisions";
18
+ export interface HookDecisions {
19
+ allow: number;
20
+ deny: number;
21
+ }
22
+ /** Cumulative hook decisions for a project, or null when unknown. */
23
+ export declare function readHookDecisions(root: string): HookDecisions | null;
24
+ /** Increment the allow or deny counter. Never throws; never touches decisions. */
25
+ export declare function recordHookDecision(root: string, allow: boolean): void;
17
26
  //# sourceMappingURL=paths.d.ts.map
package/dist/paths.js CHANGED
@@ -102,4 +102,63 @@ export function readHookHeartbeat(root) {
102
102
  return null;
103
103
  }
104
104
  }
105
+ // --- Hook decision counters --------------------------------------------------
106
+ //
107
+ // The heartbeat proves the hook RAN; it says nothing about whether the matcher
108
+ // ever fires. These cumulative allow/deny counters answer that next question:
109
+ // a hook that runs but decides wrongly (or is neutered to allow everything)
110
+ // shows allow > 0 with deny stuck at 0. They prove the matcher FIRES, not that
111
+ // every decision is right — a same-uid agent can rewrite them, so they are
112
+ // observational and advisory, exactly like the heartbeat.
113
+ //
114
+ // Deliberately a SECOND file, not a second line in the heartbeat: older
115
+ // readers parse the heartbeat as a bare timestamp, and an unreadable new
116
+ // format would regress them to "never ran". An absent counter file reads as
117
+ // null ("unknown", pre-counters hook), never as zero.
118
+ //
119
+ // Counts are approximate under concurrent hook invocations (read-modify-write
120
+ // can lose an increment). Direction survives imprecision: deny > 0 still
121
+ // proves a deny happened.
122
+ export const HOOK_DECISIONS_FILE = 'hook-decisions';
123
+ function isHookDecisions(value) {
124
+ if (typeof value !== 'object' || value === null)
125
+ return false;
126
+ const rec = value;
127
+ return (typeof rec.allow === 'number' &&
128
+ Number.isInteger(rec.allow) &&
129
+ rec.allow >= 0 &&
130
+ typeof rec.deny === 'number' &&
131
+ Number.isInteger(rec.deny) &&
132
+ rec.deny >= 0);
133
+ }
134
+ /** Cumulative hook decisions for a project, or null when unknown. */
135
+ export function readHookDecisions(root) {
136
+ try {
137
+ const raw = readFileSync(join(resolve(root), '.envseal', HOOK_DECISIONS_FILE), 'utf8');
138
+ const parsed = JSON.parse(raw);
139
+ return isHookDecisions(parsed) ? parsed : null;
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ /** Increment the allow or deny counter. Never throws; never touches decisions. */
146
+ export function recordHookDecision(root, allow) {
147
+ try {
148
+ const stateDir = join(resolve(root), '.envseal');
149
+ const marker = join(stateDir, HOOK_DECISIONS_FILE);
150
+ const current = readHookDecisions(root) ?? { allow: 0, deny: 0 };
151
+ if (allow) {
152
+ current.allow += 1;
153
+ }
154
+ else {
155
+ current.deny += 1;
156
+ }
157
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
158
+ writeFileSync(marker, `${JSON.stringify(current)}\n`, { mode: 0o600 });
159
+ }
160
+ catch {
161
+ // Observational only — an unwritable counter changes nothing.
162
+ }
163
+ }
105
164
  //# sourceMappingURL=paths.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envseal/core",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.js",
@@ -22,10 +22,10 @@
22
22
  "dependencies": {
23
23
  "jsonc-parser": "^3.3.1",
24
24
  "ulid": "^2.3.0",
25
- "@envseal/protocol": "0.1.6",
26
- "@envseal/detector": "0.1.6",
27
- "@envseal/registry": "0.1.6",
28
- "@envseal/prompters": "0.1.6"
25
+ "@envseal/protocol": "0.1.7",
26
+ "@envseal/detector": "0.1.7",
27
+ "@envseal/registry": "0.1.7",
28
+ "@envseal/prompters": "0.1.7"
29
29
  },
30
30
  "devDependencies": {
31
31
  "fast-check": "^3.23.1"