@directive-run/knowledge 1.4.0 → 1.5.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.
@@ -12,6 +12,7 @@
12
12
  import {
13
13
  type InjectionDetectionResult,
14
14
  type PIIDetectionResult,
15
+ detectAndRedactPII,
15
16
  detectPII,
16
17
  detectPromptInjection,
17
18
  } from "@directive-run/ai";
@@ -169,7 +170,84 @@ export const system = createSystem({
169
170
  // Analysis Functions
170
171
  // ============================================================================
171
172
 
172
- export function analyzeMessage(text: string): ChatMessage {
173
+ /**
174
+ * Compliance gate: blocks the message when the active mode forbids the kinds
175
+ * of PII detected. Emits its own devtools event and returns whether it blocked.
176
+ */
177
+ function runComplianceCheck(
178
+ piiResult: PIIDetectionResult,
179
+ textLength: number,
180
+ ): boolean {
181
+ const mode = system.facts.complianceMode as ComplianceMode;
182
+ let blocked = false;
183
+
184
+ if (mode !== "standard" && piiResult.detected) {
185
+ // HIPAA Safe Harbor (45 CFR 164.514) enumerates 18 identifier classes —
186
+ // names, geographic data, dates, contact info, account/certificate/license
187
+ // numbers, IP addresses, and more. That is effectively every PII type this
188
+ // detector emits, so under HIPAA *any* detected PII is PHI and blocks.
189
+ const hasPHI = piiResult.detected;
190
+ const hasContactInfo = piiResult.items.some(
191
+ (i) =>
192
+ i.type === "email" ||
193
+ i.type === "phone" ||
194
+ i.type === "name" ||
195
+ i.type === "date_of_birth" ||
196
+ i.type === "address",
197
+ );
198
+
199
+ if (mode === "hipaa" && hasPHI) {
200
+ blocked = true;
201
+ system.facts.complianceBlocks =
202
+ (system.facts.complianceBlocks as number) + 1;
203
+ addTimeline("compliance", "HIPAA: PHI detected", "compliance");
204
+ }
205
+
206
+ if (mode === "gdpr" && hasContactInfo) {
207
+ blocked = true;
208
+ system.facts.complianceBlocks =
209
+ (system.facts.complianceBlocks as number) + 1;
210
+ addTimeline("compliance", "GDPR: personal data detected", "compliance");
211
+ }
212
+ }
213
+
214
+ emitDevToolsEvent({
215
+ type: "guardrail_check",
216
+ guardrailName: `compliance-${mode}`,
217
+ guardrailType: "input",
218
+ passed: !blocked || !piiResult.detected,
219
+ inputLength: textLength,
220
+ });
221
+
222
+ return blocked;
223
+ }
224
+
225
+ /**
226
+ * When redaction is on, never persist raw PII into reactive facts: strip the
227
+ * plaintext `value` from each detected item before it reaches facts.
228
+ */
229
+ function toSafePiiResult(
230
+ piiResult: PIIDetectionResult,
231
+ redactionEnabled: boolean,
232
+ ): PIIDetectionResult | null {
233
+ if (!piiResult.detected) {
234
+ return null;
235
+ }
236
+
237
+ if (!redactionEnabled) {
238
+ return piiResult;
239
+ }
240
+
241
+ return {
242
+ ...piiResult,
243
+ items: piiResult.items.map((item) => ({
244
+ ...item,
245
+ value: "[redacted]",
246
+ })),
247
+ };
248
+ }
249
+
250
+ export async function analyzeMessage(text: string): Promise<ChatMessage> {
173
251
  const id = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
174
252
  let blocked = false;
175
253
 
@@ -192,11 +270,12 @@ export function analyzeMessage(text: string): ChatMessage {
192
270
  inputLength: text.length,
193
271
  });
194
272
 
195
- // 2. PII detection
196
- const piiResult = detectPII(text, {
197
- redact: system.facts.redactionEnabled as boolean,
198
- redactionStyle: "typed",
199
- });
273
+ // 2. PII detection. detectPII is detection-only; detectAndRedactPII also
274
+ // populates `redactedText` so we never have to mutate a returned result.
275
+ const redactionEnabled = system.facts.redactionEnabled;
276
+ const piiResult = redactionEnabled
277
+ ? await detectAndRedactPII(text, { style: "typed" })
278
+ : await detectPII(text);
200
279
  if (piiResult.detected) {
201
280
  system.facts.piiDetections = (system.facts.piiDetections as number) + 1;
202
281
  for (const item of piiResult.items) {
@@ -213,41 +292,10 @@ export function analyzeMessage(text: string): ChatMessage {
213
292
  });
214
293
 
215
294
  // 3. Compliance check
216
- const mode = system.facts.complianceMode as ComplianceMode;
217
- if (mode !== "standard" && piiResult.detected) {
218
- const hasPHI = piiResult.items.some(
219
- (i) =>
220
- i.type === "medical_id" ||
221
- i.type === "ssn" ||
222
- i.type === "date_of_birth",
223
- );
224
- const hasContactInfo = piiResult.items.some(
225
- (i) => i.type === "email" || i.type === "phone" || i.type === "name",
226
- );
227
-
228
- if (mode === "hipaa" && hasPHI) {
229
- blocked = true;
230
- system.facts.complianceBlocks =
231
- (system.facts.complianceBlocks as number) + 1;
232
- addTimeline("compliance", "HIPAA: PHI detected", "compliance");
233
- }
234
-
235
- if (mode === "gdpr" && hasContactInfo) {
236
- blocked = true;
237
- system.facts.complianceBlocks =
238
- (system.facts.complianceBlocks as number) + 1;
239
- addTimeline("compliance", "GDPR: personal data detected", "compliance");
240
- }
295
+ if (runComplianceCheck(piiResult, text.length)) {
296
+ blocked = true;
241
297
  }
242
298
 
243
- emitDevToolsEvent({
244
- type: "guardrail_check",
245
- guardrailName: `compliance-${mode}`,
246
- guardrailType: "input",
247
- passed: !blocked || !piiResult.detected,
248
- inputLength: text.length,
249
- });
250
-
251
299
  if (blocked) {
252
300
  system.facts.blockedCount = (system.facts.blockedCount as number) + 1;
253
301
  }
@@ -260,10 +308,10 @@ export function analyzeMessage(text: string): ChatMessage {
260
308
 
261
309
  return {
262
310
  id,
263
- text,
311
+ text: redactionEnabled ? redactedText : text,
264
312
  blocked,
265
313
  redactedText,
266
314
  injectionResult: injectionResult.detected ? injectionResult : null,
267
- piiResult: piiResult.detected ? piiResult : null,
315
+ piiResult: toSafePiiResult(piiResult, redactionEnabled),
268
316
  };
269
317
  }
@@ -48,8 +48,8 @@ import {
48
48
  } from "@directive-run/core/plugins";
49
49
  // createObservability is alpha (not in bundle) — direct source import
50
50
  import {
51
- createObservability,
52
51
  createAgentMetrics,
52
+ createObservability,
53
53
  } from "../../../packages/core/src/plugins/observability.lab.js";
54
54
  import {
55
55
  analysisAgent,
@@ -6,7 +6,12 @@
6
6
  * Shared counter module — same file used by all framework examples.
7
7
  */
8
8
 
9
- import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
9
+ import {
10
+ type ModuleSchema,
11
+ createModule,
12
+ createSystem,
13
+ t,
14
+ } from "@directive-run/core";
10
15
 
11
16
  const schema = {
12
17
  facts: { count: t.number() },
@@ -17,15 +22,23 @@ const schema = {
17
22
 
18
23
  export const counterModule = createModule("counter", {
19
24
  schema,
20
- init: (facts) => { facts.count = 0; },
25
+ init: (facts) => {
26
+ facts.count = 0;
27
+ },
21
28
  derive: {
22
29
  doubled: (facts) => facts.count * 2,
23
30
  isPositive: (facts) => facts.count > 0,
24
31
  },
25
32
  events: {
26
- increment: (facts) => { facts.count += 1; },
27
- decrement: (facts) => { facts.count -= 1; },
28
- reset: (facts) => { facts.count = 0; },
33
+ increment: (facts) => {
34
+ facts.count += 1;
35
+ },
36
+ decrement: (facts) => {
37
+ facts.count -= 1;
38
+ },
39
+ reset: (facts) => {
40
+ facts.count = 0;
41
+ },
29
42
  },
30
43
  constraints: {
31
44
  noNegative: {
@@ -36,7 +49,9 @@ export const counterModule = createModule("counter", {
36
49
  resolvers: {
37
50
  clamp: {
38
51
  requirement: "CLAMP_TO_ZERO",
39
- resolve: async (req, context) => { context.facts.count = 0; },
52
+ resolve: async (req, context) => {
53
+ context.facts.count = 0;
54
+ },
40
55
  },
41
56
  },
42
57
  });
@@ -6,7 +6,12 @@
6
6
  * Shared counter module — same file used by all framework examples.
7
7
  */
8
8
 
9
- import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
9
+ import {
10
+ type ModuleSchema,
11
+ createModule,
12
+ createSystem,
13
+ t,
14
+ } from "@directive-run/core";
10
15
 
11
16
  const schema = {
12
17
  facts: { count: t.number() },
@@ -17,15 +22,23 @@ const schema = {
17
22
 
18
23
  export const counterModule = createModule("counter", {
19
24
  schema,
20
- init: (facts) => { facts.count = 0; },
25
+ init: (facts) => {
26
+ facts.count = 0;
27
+ },
21
28
  derive: {
22
29
  doubled: (facts) => facts.count * 2,
23
30
  isPositive: (facts) => facts.count > 0,
24
31
  },
25
32
  events: {
26
- increment: (facts) => { facts.count += 1; },
27
- decrement: (facts) => { facts.count -= 1; },
28
- reset: (facts) => { facts.count = 0; },
33
+ increment: (facts) => {
34
+ facts.count += 1;
35
+ },
36
+ decrement: (facts) => {
37
+ facts.count -= 1;
38
+ },
39
+ reset: (facts) => {
40
+ facts.count = 0;
41
+ },
29
42
  },
30
43
  constraints: {
31
44
  noNegative: {
@@ -36,7 +49,9 @@ export const counterModule = createModule("counter", {
36
49
  resolvers: {
37
50
  clamp: {
38
51
  requirement: "CLAMP_TO_ZERO",
39
- resolve: async (req, context) => { context.facts.count = 0; },
52
+ resolve: async (req, context) => {
53
+ context.facts.count = 0;
54
+ },
40
55
  },
41
56
  },
42
57
  });
@@ -6,7 +6,12 @@
6
6
  * Shared counter module — same file used by all framework examples.
7
7
  */
8
8
 
9
- import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
9
+ import {
10
+ type ModuleSchema,
11
+ createModule,
12
+ createSystem,
13
+ t,
14
+ } from "@directive-run/core";
10
15
 
11
16
  const schema = {
12
17
  facts: { count: t.number() },
@@ -17,15 +22,23 @@ const schema = {
17
22
 
18
23
  export const counterModule = createModule("counter", {
19
24
  schema,
20
- init: (facts) => { facts.count = 0; },
25
+ init: (facts) => {
26
+ facts.count = 0;
27
+ },
21
28
  derive: {
22
29
  doubled: (facts) => facts.count * 2,
23
30
  isPositive: (facts) => facts.count > 0,
24
31
  },
25
32
  events: {
26
- increment: (facts) => { facts.count += 1; },
27
- decrement: (facts) => { facts.count -= 1; },
28
- reset: (facts) => { facts.count = 0; },
33
+ increment: (facts) => {
34
+ facts.count += 1;
35
+ },
36
+ decrement: (facts) => {
37
+ facts.count -= 1;
38
+ },
39
+ reset: (facts) => {
40
+ facts.count = 0;
41
+ },
29
42
  },
30
43
  constraints: {
31
44
  noNegative: {
@@ -36,7 +49,9 @@ export const counterModule = createModule("counter", {
36
49
  resolvers: {
37
50
  clamp: {
38
51
  requirement: "CLAMP_TO_ZERO",
39
- resolve: async (req, context) => { context.facts.count = 0; },
52
+ resolve: async (req, context) => {
53
+ context.facts.count = 0;
54
+ },
40
55
  },
41
56
  },
42
57
  });
@@ -9,7 +9,12 @@
9
9
  * Total: ~40 lines.
10
10
  */
11
11
 
12
- import { createModule, createSystem, t, type ModuleSchema } from "@directive-run/core";
12
+ import {
13
+ type ModuleSchema,
14
+ createModule,
15
+ createSystem,
16
+ t,
17
+ } from "@directive-run/core";
13
18
 
14
19
  const schema = {
15
20
  facts: {
@@ -42,9 +47,15 @@ export const counterModule = createModule("counter", {
42
47
  },
43
48
 
44
49
  events: {
45
- increment: (facts) => { facts.count += 1; },
46
- decrement: (facts) => { facts.count -= 1; },
47
- reset: (facts) => { facts.count = 0; },
50
+ increment: (facts) => {
51
+ facts.count += 1;
52
+ },
53
+ decrement: (facts) => {
54
+ facts.count -= 1;
55
+ },
56
+ reset: (facts) => {
57
+ facts.count = 0;
58
+ },
48
59
  },
49
60
 
50
61
  // When count goes negative, automatically fix it
@@ -0,0 +1,175 @@
1
+ // Example: data-triggers
2
+ // Source: examples/data-triggers/src/index.ts
3
+ // Extracted for AI rules — DOM wiring stripped
4
+
5
+ /**
6
+ * All five Directive definition surfaces written as DATA.
7
+ *
8
+ * Run with: `pnpm --filter @directive-run/example-data-triggers start`
9
+ *
10
+ * Demonstrates:
11
+ * - constraint `when: { ... }`
12
+ * - effect `on: { ... }`
13
+ * - resolver `key: [ ... ]`
14
+ * - event `patch: { $set: { ... } }`
15
+ * - derive `compute: { ... }` (predicate + template)
16
+ *
17
+ * And the introspection that only a data form makes possible:
18
+ * - `system.inspect().constraints[].whenSpec`
19
+ * - `constraint.evaluate` observer event with `whenExplain`
20
+ * - `system.explain()` rendering a clause-by-clause ✓/✗ tree.
21
+ */
22
+
23
+ import {
24
+ type ModuleSchema,
25
+ createModule,
26
+ createSystem,
27
+ t,
28
+ } from "@directive-run/core";
29
+
30
+ // ----------------------------------------------------------------------------
31
+ // Module
32
+ // ----------------------------------------------------------------------------
33
+
34
+ const schema = {
35
+ facts: {
36
+ phase: t.string<"red" | "yellow" | "green">(),
37
+ elapsed: t.number(),
38
+ transitionCount: t.number(),
39
+ label: t.string(),
40
+ firstName: t.string(),
41
+ lastName: t.string(),
42
+ age: t.number(),
43
+ },
44
+ derivations: {
45
+ isAdult: t.boolean(),
46
+ fullName: t.string(),
47
+ },
48
+ events: {
49
+ advanceTo: { value: t.string(), userName: t.string() },
50
+ },
51
+ requirements: {
52
+ TRANSITION: { to: t.string<"red" | "yellow" | "green">() },
53
+ },
54
+ } satisfies ModuleSchema;
55
+
56
+ const trafficLight = createModule("traffic", {
57
+ schema,
58
+
59
+ init: (facts) => {
60
+ facts.phase = "red";
61
+ facts.elapsed = 0;
62
+ facts.transitionCount = 0;
63
+ facts.label = "";
64
+ facts.firstName = "Grace";
65
+ facts.lastName = "Hopper";
66
+ facts.age = 30;
67
+ },
68
+
69
+ // ── Derivations — predicate + template, no functions ─────────────────────
70
+ derive: {
71
+ isAdult: { compute: { age: { $gte: 18 } } },
72
+ fullName: { compute: { $template: "${firstName} ${lastName}" } },
73
+ },
74
+
75
+ // ── Event — declarative patch instead of a handler ────────────────────────
76
+ events: {
77
+ advanceTo: {
78
+ patch: {
79
+ $set: {
80
+ phase: { $ref: "value" },
81
+ elapsed: 0,
82
+ label: { $template: "Set by ${userName} → ${value}" },
83
+ },
84
+ },
85
+ },
86
+ },
87
+
88
+ // ── Constraint — data `when`, declarative `require` ──────────────────────
89
+ constraints: {
90
+ transition: {
91
+ when: { phase: "red", elapsed: { $gte: 30 } },
92
+ require: { type: "TRANSITION", to: "green" },
93
+ },
94
+ },
95
+
96
+ // ── Resolver — KeySelector array dedups by requirement field ─────────────
97
+ resolvers: {
98
+ transition: {
99
+ requirement: "TRANSITION",
100
+ key: ["to"],
101
+ resolve: async (req, ctx) => {
102
+ ctx.facts.phase = req.to;
103
+ ctx.facts.elapsed = 0;
104
+ ctx.facts.transitionCount = ctx.facts.transitionCount + 1;
105
+ },
106
+ },
107
+ },
108
+ });
109
+
110
+ // ----------------------------------------------------------------------------
111
+ // System + observer
112
+ // ----------------------------------------------------------------------------
113
+
114
+ const system = createSystem({ module: trafficLight });
115
+
116
+ system.observe((event) => {
117
+ if (event.type === "constraint.evaluate" && event.whenExplain) {
118
+ console.log(
119
+ `\n[observe] constraint.evaluate ${event.id} → ${event.active}`,
120
+ );
121
+ for (const clause of event.whenExplain) {
122
+ const mark = clause.pass ? "✓" : "✗";
123
+ console.log(
124
+ ` ${mark} ${clause.path} ${clause.op} ${JSON.stringify(clause.expected)} (actual: ${JSON.stringify(clause.actual)})`,
125
+ );
126
+ }
127
+ }
128
+ });
129
+
130
+ system.start();
131
+
132
+ // ----------------------------------------------------------------------------
133
+ // Run
134
+ // ----------------------------------------------------------------------------
135
+
136
+ console.log("--- Initial derivations (data forms) ---");
137
+ console.log(`isAdult: ${system.derive.isAdult}`);
138
+ console.log(`fullName: ${system.derive.fullName}`);
139
+
140
+ console.log("\n--- inspect().constraints[] surfaces whenSpec ---");
141
+ for (const c of system.inspect().constraints) {
142
+ console.log(`${c.id}: whenSpec = ${JSON.stringify(c.whenSpec)}`);
143
+ }
144
+
145
+ // Trip the predicate: elapsed goes from 0 → 30, so the constraint fires.
146
+ console.log("\n--- elapsed → 30 (predicate now holds) ---");
147
+ system.facts.elapsed = 30;
148
+ await settle();
149
+
150
+ console.log(`phase after resolver: ${system.facts.phase}`);
151
+ console.log(`transitionCount: ${system.facts.transitionCount}`);
152
+
153
+ // Show explain() on the next emitted requirement (if any), or on a re-fire.
154
+ // Two writes: phase ← "red" satisfies the first clause; elapsed ← 5 fails the
155
+ // `$gte: 30` clause, so the predicate as a whole is false and the constraint
156
+ // does not fire.
157
+ console.log("\n--- phase = red, elapsed → 5 (predicate fails on elapsed) ---");
158
+ system.facts.phase = "red";
159
+ system.facts.elapsed = 5;
160
+ await settle();
161
+
162
+ // Dispatch the data-form event — sets multiple facts from the payload.
163
+ console.log("\n--- dispatch advanceTo (patch) ---");
164
+ system.events.advanceTo({ value: "yellow", userName: "Ada" });
165
+ await settle();
166
+ console.log(`phase: ${system.facts.phase}`);
167
+ console.log(`label: ${system.facts.label}`);
168
+
169
+ system.destroy();
170
+
171
+ async function settle(): Promise<void> {
172
+ await new Promise((r) => setTimeout(r, 0));
173
+ await Promise.resolve();
174
+ await new Promise((r) => setTimeout(r, 0));
175
+ }
@@ -110,12 +110,7 @@ export const sudokuSchema = {
110
110
  export const sudokuGame = createModule("sudoku", {
111
111
  schema: sudokuSchema,
112
112
  history: {
113
- snapshotEvents: [
114
- "inputNumber",
115
- "toggleNote",
116
- "requestHint",
117
- "newGame",
118
- ],
113
+ snapshotEvents: ["inputNumber", "toggleNote", "requestHint", "newGame"],
119
114
  },
120
115
 
121
116
  init: (facts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directive-run/knowledge",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Knowledge files, examples, and validation for Directive — the constraint-driven TypeScript runtime.",
5
5
  "license": "(MIT OR Apache-2.0)",
6
6
  "author": "Jason Comes",
@@ -51,8 +51,8 @@
51
51
  "tsx": "^4.19.2",
52
52
  "typescript": "^5.7.2",
53
53
  "vitest": "^3.0.0",
54
- "@directive-run/core": "1.4.0",
55
- "@directive-run/ai": "1.4.0"
54
+ "@directive-run/core": "1.5.0",
55
+ "@directive-run/ai": "1.5.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsx scripts/generate-api-skeleton.ts && tsx scripts/generate-sitemap.ts && tsx scripts/extract-examples.ts && tsup",