@bernierllc/nevar-loop-detector 0.0.1 → 0.1.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/README.md CHANGED
@@ -1,45 +1,71 @@
1
1
  # @bernierllc/nevar-loop-detector
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Static and runtime loop detection for the Nevar rules engine. Analyzes rule sets for potential evaluation cycles at registration time and tracks runtime emit chains.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ ## Installation
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ ```bash
8
+ npm install @bernierllc/nevar-loop-detector
9
+ ```
8
10
 
9
- ## Purpose
11
+ ## Usage
10
12
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@bernierllc/nevar-loop-detector`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
13
+ ```typescript
14
+ import { LoopDetector, ChainTracker } from '@bernierllc/nevar-loop-detector';
15
+ import type { Rule } from '@bernierllc/nevar-types';
15
16
 
16
- ## What is OIDC Trusted Publishing?
17
+ // Static analysis: detect cycles in rule definitions
18
+ const detector = new LoopDetector();
19
+ const analysis = detector.analyze(rules);
17
20
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
21
+ if (analysis.hasLoops) {
22
+ for (const loop of analysis.loops) {
23
+ console.log('Cycle:', loop.chain.join(' -> '));
24
+ console.log('All rules opted in:', loop.allOptedIn);
25
+ console.log('Rule IDs:', loop.ruleIds);
26
+ }
27
+ }
19
28
 
20
- ## Setup Instructions
29
+ // Runtime tracking: detect cycles during emit chain execution
30
+ const tracker = new ChainTracker();
21
31
 
22
- To properly configure OIDC trusted publishing for this package:
32
+ const isCycle = tracker.enter('order.created', 'ctx-123');
33
+ if (isCycle) {
34
+ // This trigger+context pair was already seen in this chain
35
+ }
23
36
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
37
+ console.log(tracker.getDepth()); // current chain depth
38
+ tracker.reset(); // clear for next chain
39
+ ```
28
40
 
29
- ## DO NOT USE THIS PACKAGE
41
+ ## API
30
42
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
43
+ ### `LoopDetector`
36
44
 
37
- ## More Information
45
+ Static loop analysis for rule sets. Builds a graph of trigger-to-deferred-trigger relationships and detects cycles using depth-first search.
38
46
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
47
+ - `analyze(rules: Rule[])` - Returns a `LoopAnalysis` with `hasLoops: boolean` and `loops: DetectedLoop[]`. Each `DetectedLoop` contains the `chain` of trigger keys forming the cycle, `ruleIds` involved, and whether `allOptedIn` to looping.
42
48
 
43
- ---
49
+ ### `ChainTracker`
44
50
 
45
- **Maintained for OIDC setup purposes only**
51
+ Runtime cycle detection for emit chains. Tracks `(triggerKey, contextId)` pairs within a single emit chain.
52
+
53
+ - `enter(triggerKey, contextId)` - Records entry into a trigger evaluation. Returns `true` if a cycle is detected (pair already seen), `false` on first occurrence
54
+ - `getDepth()` - Returns the current chain depth
55
+ - `reset()` - Clears all recorded entries and resets depth
56
+
57
+ ### `LoopDetectorError`
58
+
59
+ Error class for loop detection failures. Extends `Error` with `code` and `context` properties.
60
+
61
+ ## Integration Documentation
62
+
63
+ ### Logger Integration
64
+ This package does not integrate with `@bernierllc/logger`. As a core package, logger integration is optional and not included by default. Consumers should handle logging at the service layer.
65
+
66
+ ### NeverHub Integration
67
+ This package does not integrate with `@bernierllc/neverhub-adapter`. As a core package, NeverHub integration is not applicable. NeverHub registration should be handled by service-layer packages that compose this package.
68
+
69
+ ## License
70
+
71
+ Copyright (c) 2025 Bernier LLC. All rights reserved.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Runtime cycle detection for emit chains.
3
+ *
4
+ * Tracks (triggerKey, contextId) pairs within a single emit chain
5
+ * to detect runtime loops.
6
+ */
7
+ export declare class ChainTracker {
8
+ private seen;
9
+ private depth;
10
+ /**
11
+ * Record entry into a trigger evaluation.
12
+ *
13
+ * @param triggerKey - The trigger key being evaluated
14
+ * @param contextId - A unique context identifier for the emit chain
15
+ * @returns true if this (triggerKey, contextId) pair was already seen (cycle detected),
16
+ * false if this is the first occurrence
17
+ */
18
+ enter(triggerKey: string, contextId: string): boolean;
19
+ /**
20
+ * Get the current chain depth.
21
+ */
22
+ getDepth(): number;
23
+ /**
24
+ * Reset the tracker, clearing all recorded entries and depth.
25
+ */
26
+ reset(): void;
27
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ChainTracker = void 0;
11
+ /**
12
+ * Runtime cycle detection for emit chains.
13
+ *
14
+ * Tracks (triggerKey, contextId) pairs within a single emit chain
15
+ * to detect runtime loops.
16
+ */
17
+ class ChainTracker {
18
+ seen = new Set();
19
+ depth = 0;
20
+ /**
21
+ * Record entry into a trigger evaluation.
22
+ *
23
+ * @param triggerKey - The trigger key being evaluated
24
+ * @param contextId - A unique context identifier for the emit chain
25
+ * @returns true if this (triggerKey, contextId) pair was already seen (cycle detected),
26
+ * false if this is the first occurrence
27
+ */
28
+ enter(triggerKey, contextId) {
29
+ const key = `${triggerKey}::${contextId}`;
30
+ if (this.seen.has(key)) {
31
+ return true;
32
+ }
33
+ this.seen.add(key);
34
+ this.depth++;
35
+ return false;
36
+ }
37
+ /**
38
+ * Get the current chain depth.
39
+ */
40
+ getDepth() {
41
+ return this.depth;
42
+ }
43
+ /**
44
+ * Reset the tracker, clearing all recorded entries and depth.
45
+ */
46
+ reset() {
47
+ this.seen.clear();
48
+ this.depth = 0;
49
+ }
50
+ }
51
+ exports.ChainTracker = ChainTracker;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Error class for loop detection failures.
3
+ */
4
+ export declare class LoopDetectorError extends Error {
5
+ readonly code: string;
6
+ readonly context?: Record<string, unknown>;
7
+ constructor(message: string, options?: {
8
+ cause?: Error;
9
+ code?: string;
10
+ context?: Record<string, unknown>;
11
+ });
12
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LoopDetectorError = void 0;
11
+ /**
12
+ * Error class for loop detection failures.
13
+ */
14
+ class LoopDetectorError extends Error {
15
+ code;
16
+ context;
17
+ constructor(message, options) {
18
+ super(message, { cause: options?.cause });
19
+ this.name = 'LoopDetectorError';
20
+ this.code = options?.code ?? 'LOOP_DETECTOR_ERROR';
21
+ this.context = options?.context;
22
+ }
23
+ }
24
+ exports.LoopDetectorError = LoopDetectorError;
@@ -0,0 +1,3 @@
1
+ export { LoopDetector } from './loop-detector';
2
+ export { ChainTracker } from './chain-tracker';
3
+ export { LoopDetectorError } from './errors';
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LoopDetectorError = exports.ChainTracker = exports.LoopDetector = void 0;
11
+ var loop_detector_1 = require("./loop-detector");
12
+ Object.defineProperty(exports, "LoopDetector", { enumerable: true, get: function () { return loop_detector_1.LoopDetector; } });
13
+ var chain_tracker_1 = require("./chain-tracker");
14
+ Object.defineProperty(exports, "ChainTracker", { enumerable: true, get: function () { return chain_tracker_1.ChainTracker; } });
15
+ var errors_1 = require("./errors");
16
+ Object.defineProperty(exports, "LoopDetectorError", { enumerable: true, get: function () { return errors_1.LoopDetectorError; } });
@@ -0,0 +1,21 @@
1
+ import type { Rule, LoopAnalysis } from '@bernierllc/nevar-types';
2
+ /**
3
+ * Static loop analysis for rule sets.
4
+ *
5
+ * Walks trigger -> action -> deferredTrigger chains across rules
6
+ * to detect cycles at registration time.
7
+ */
8
+ export declare class LoopDetector {
9
+ /**
10
+ * Analyze a set of rules for potential evaluation loops.
11
+ *
12
+ * Builds a graph of triggerType -> deferredTrigger relationships
13
+ * and detects cycles using DFS.
14
+ */
15
+ analyze(rules: Rule[]): LoopAnalysis;
16
+ private buildEdges;
17
+ private extractDeferredTriggers;
18
+ private findCycles;
19
+ private dfs;
20
+ private deduplicateLoops;
21
+ }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LoopDetector = void 0;
11
+ /**
12
+ * Static loop analysis for rule sets.
13
+ *
14
+ * Walks trigger -> action -> deferredTrigger chains across rules
15
+ * to detect cycles at registration time.
16
+ */
17
+ class LoopDetector {
18
+ /**
19
+ * Analyze a set of rules for potential evaluation loops.
20
+ *
21
+ * Builds a graph of triggerType -> deferredTrigger relationships
22
+ * and detects cycles using DFS.
23
+ */
24
+ analyze(rules) {
25
+ const edges = this.buildEdges(rules);
26
+ const ruleMap = new Map(rules.map((r) => [r.id, r]));
27
+ const loops = this.findCycles(edges, ruleMap);
28
+ return {
29
+ hasLoops: loops.length > 0,
30
+ loops,
31
+ };
32
+ }
33
+ buildEdges(rules) {
34
+ const edges = new Map();
35
+ for (const rule of rules) {
36
+ if (!rule.isActive) {
37
+ continue;
38
+ }
39
+ for (const action of rule.actions) {
40
+ const config = action.config;
41
+ const deferredTriggers = this.extractDeferredTriggers(config);
42
+ for (const triggerKey of deferredTriggers) {
43
+ if (!edges.has(rule.triggerType)) {
44
+ edges.set(rule.triggerType, []);
45
+ }
46
+ edges.get(rule.triggerType).push({
47
+ triggerKey,
48
+ ruleId: rule.id,
49
+ allowLoop: rule.allowLoop,
50
+ });
51
+ }
52
+ }
53
+ }
54
+ return edges;
55
+ }
56
+ extractDeferredTriggers(config) {
57
+ const triggers = [];
58
+ if (config['deferredTrigger'] && typeof config['deferredTrigger'] === 'object') {
59
+ const dt = config['deferredTrigger'];
60
+ if (typeof dt['triggerKey'] === 'string') {
61
+ triggers.push(dt['triggerKey']);
62
+ }
63
+ }
64
+ if (Array.isArray(config['deferredTriggers'])) {
65
+ for (const dt of config['deferredTriggers']) {
66
+ if (dt && typeof dt === 'object' && typeof dt['triggerKey'] === 'string') {
67
+ triggers.push(dt['triggerKey']);
68
+ }
69
+ }
70
+ }
71
+ return triggers;
72
+ }
73
+ findCycles(edges, ruleMap) {
74
+ const loops = [];
75
+ const allTriggerTypes = new Set();
76
+ for (const [source, targets] of edges) {
77
+ allTriggerTypes.add(source);
78
+ for (const t of targets) {
79
+ allTriggerTypes.add(t.triggerKey);
80
+ }
81
+ }
82
+ for (const startNode of allTriggerTypes) {
83
+ this.dfs(startNode, edges, ruleMap, [], [], new Set(), loops);
84
+ }
85
+ return this.deduplicateLoops(loops);
86
+ }
87
+ dfs(current, edges, ruleMap, path, pathRuleIds, visited, loops) {
88
+ const cycleStart = path.indexOf(current);
89
+ if (cycleStart !== -1) {
90
+ const cyclePath = [...path.slice(cycleStart), current];
91
+ const cycleRuleIds = pathRuleIds.slice(cycleStart);
92
+ const allOptedIn = cycleRuleIds.every((ruleId) => {
93
+ const rule = ruleMap.get(ruleId);
94
+ return rule?.allowLoop === true;
95
+ });
96
+ loops.push({
97
+ chain: cyclePath,
98
+ allOptedIn,
99
+ ruleIds: cycleRuleIds,
100
+ });
101
+ return;
102
+ }
103
+ if (visited.has(current)) {
104
+ return;
105
+ }
106
+ const targets = edges.get(current);
107
+ if (!targets) {
108
+ return;
109
+ }
110
+ visited.add(current);
111
+ for (const target of targets) {
112
+ this.dfs(target.triggerKey, edges, ruleMap, [...path, current], [...pathRuleIds, target.ruleId], new Set(visited), loops);
113
+ }
114
+ }
115
+ deduplicateLoops(loops) {
116
+ const seen = new Set();
117
+ const unique = [];
118
+ for (const loop of loops) {
119
+ // Rotate chain to start with the lexicographically smallest element for stable dedup
120
+ const chain = loop.chain.slice(0, -1);
121
+ const minIdx = chain.indexOf(chain.reduce((min, val) => (val < min ? val : min), chain[0]));
122
+ const normalized = [
123
+ ...chain.slice(minIdx),
124
+ ...chain.slice(0, minIdx),
125
+ chain[minIdx],
126
+ ];
127
+ const key = normalized.join('->');
128
+ if (!seen.has(key)) {
129
+ seen.add(key);
130
+ unique.push(loop);
131
+ }
132
+ }
133
+ return unique;
134
+ }
135
+ }
136
+ exports.LoopDetector = LoopDetector;
package/package.json CHANGED
@@ -1,10 +1,56 @@
1
1
  {
2
2
  "name": "@bernierllc/nevar-loop-detector",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @bernierllc/nevar-loop-detector",
3
+ "version": "0.1.0",
4
+ "description": "Static loop analysis and runtime cycle detection for the Nevar rules engine",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/**/*",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
5
12
  "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
10
- }
13
+ "nevar",
14
+ "rules-engine",
15
+ "loop-detection",
16
+ "cycle-detection",
17
+ "static-analysis"
18
+ ],
19
+ "author": "Bernier LLC",
20
+ "license": "SEE LICENSE IN LICENSE",
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org/"
24
+ },
25
+ "engines": {
26
+ "node": ">=16.0.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/bernierllc/tools.git",
31
+ "directory": "packages/core/nevar-loop-detector"
32
+ },
33
+ "dependencies": {
34
+ "@bernierllc/nevar-types": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/jest": "^29.5.0",
38
+ "@types/node": "^20.0.0",
39
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
40
+ "@typescript-eslint/parser": "^6.0.0",
41
+ "eslint": "^8.0.0",
42
+ "jest": "^29.5.0",
43
+ "rimraf": "^5.0.0",
44
+ "ts-jest": "^29.1.0",
45
+ "typescript": "^5.0.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc",
49
+ "prebuild": "npm run clean",
50
+ "clean": "rimraf dist",
51
+ "test": "jest",
52
+ "test:run": "jest",
53
+ "test:coverage": "jest --coverage",
54
+ "lint": "eslint src __tests__ --ext .ts"
55
+ }
56
+ }