eyeling 1.13.5 → 1.13.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/README.md CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  A compact [Notation3 (N3)](https://notation3.org/) reasoner in **JavaScript**.
4
4
 
5
- - Single self-contained bundle (`eyeling.js`), no external runtime deps
6
- - Forward (`=>`) + backward (`<=`) chaining over Horn-style rules
7
- - CLI / npm `reason()` output is mode-dependent by default: **newly derived** forward facts in normal forward mode, or (when top-level `log:query` directives are present) the **unique instantiated conclusion triples** of those queries (optionally with compact proof comments)
8
- - If the input contains one or more top-level `{ ... } log:query { ... }.` directives, the output becomes the **unique instantiated conclusion triples** of those queries (a forward-rule-like projection)
5
+ - Single self-contained bundle (`eyeling.js`), no external runtime dependencies
6
+ - Forward (`=>`) and backward (`<=`) chaining over Horn-style rules
7
+ - **CLI / npm `reason()` output is mode-dependent by default**: it prints **newly derived forward facts** in normal mode, or (when top-level `{ ... } log:query { ... }.` directives are present) the **unique instantiated conclusion triples** of those queries, optionally with compact proof comments
9
8
  - Works in Node.js and fully client-side (browser/worker)
10
9
 
11
10
  ## Links
@@ -16,9 +15,7 @@ A compact [Notation3 (N3)](https://notation3.org/) reasoner in **JavaScript**.
16
15
  - **Notation3 test suite:** [https://codeberg.org/phochste/notation3tests](https://codeberg.org/phochste/notation3tests)
17
16
  - **Eyeling conformance report:** [https://codeberg.org/phochste/notation3tests/src/branch/main/reports/report.md](https://codeberg.org/phochste/notation3tests/src/branch/main/reports/report.md)
18
17
 
19
- Eyeling is regularly checked against the community Notation3 test suite; the report above tracks current pass/fail results.
20
-
21
- If you want to understand how the parser, unifier, proof search, skolemization, scoped closure, and builtins are implemented, start with the handbook.
18
+ Eyeling is regularly checked against the community Notation3 test suite. If you want implementation details (parser, unifier, proof search, skolemization, scoped closure, builtins), start with the handbook.
22
19
 
23
20
  ## Quick start
24
21
 
@@ -32,7 +29,7 @@ If you want to understand how the parser, unifier, proof search, skolemization,
32
29
  npm i eyeling
33
30
  ```
34
31
 
35
- ### CLI
32
+ ## CLI usage
36
33
 
37
34
  Run on a file:
38
35
 
@@ -40,23 +37,33 @@ Run on a file:
40
37
  npx eyeling examples/socrates.n3
41
38
  ```
42
39
 
43
- See all options:
40
+ Show all options:
44
41
 
45
42
  ```bash
46
43
  npx eyeling --help
47
44
  ```
48
45
 
49
- ### log:query output selection
46
+ Useful flags include `--proof-comments`, `--stream`, `--strings`, and `--enforce-https`.
47
+
48
+ ## What gets printed?
49
+
50
+ ### Normal mode (default)
51
+
52
+ Without top-level `log:query` directives, Eyeling prints **newly derived forward facts** by default.
53
+
54
+ ### `log:query` mode (output selection)
50
55
 
51
- If your input contains one or more **top-level** directives of the form:
56
+ If the input contains one or more **top-level** directives of the form:
52
57
 
53
58
  ```n3
54
59
  { ?x a :Human. } log:query { ?x a :Mortal. }.
55
60
  ```
56
61
 
57
- Eyeling will still compute the saturated forward closure, but it will **print only** the **unique instantiated conclusion triples** of those `log:query` directives (instead of printing all newly derived forward facts).
62
+ Eyeling still computes the saturated forward closure, but it **prints only** the **unique instantiated conclusion triples** of those `log:query` directives (instead of all newly derived forward facts).
58
63
 
59
- ### JavaScript API
64
+ ## JavaScript API
65
+
66
+ ### npm helper: `reason()`
60
67
 
61
68
  CommonJS:
62
69
 
@@ -70,7 +77,7 @@ const input = `
70
77
  :Socrates a :Human.
71
78
  :Human rdfs:subClassOf :Mortal.
72
79
 
73
- { ?S a ?A. ?A rdfs:subClassOf ?B } => { ?S a ?B }.
80
+ { ?s a ?A. ?A rdfs:subClassOf ?B. } => { ?s a ?B. }.
74
81
  `;
75
82
 
76
83
  console.log(reason({ proofComments: false }, input));
@@ -80,36 +87,57 @@ ESM:
80
87
 
81
88
  ```js
82
89
  import eyeling from 'eyeling';
90
+
83
91
  console.log(eyeling.reason({ proofComments: false }, input));
84
92
  ```
85
93
 
86
- Streaming / in-process reasoning (browser/worker, direct `eyeling.js`):
94
+ Notes:
95
+
96
+ - `reason()` returns the same textual output you would get from the CLI for the same input/options.
97
+ - By default, the npm helper keeps output machine-friendly (`proofComments: false`).
98
+ - The npm helper shells out to the bundled `eyeling.js` CLI for simplicity and robustness.
99
+
100
+ ### Direct bundle / browser-worker API: `reasonStream()`
101
+
102
+ For in-process reasoning (browser, worker, or direct use of `eyeling.js`):
87
103
 
88
104
  ```js
89
- const { closureN3 } = eyeling.reasonStream(input, {
105
+ const result = eyeling.reasonStream(input, {
90
106
  proof: false,
91
107
  onDerived: ({ triple }) => console.log(triple),
108
+ // includeInputFactsInClosure: false,
92
109
  });
93
110
 
94
- // By default, closureN3 includes input facts + derived facts.
95
- // To get only newly derived facts in closureN3, pass:
96
- // includeInputFactsInClosure: false
97
- // With log:query directives present, closureN3 contains the query-selected triples.
98
- // The return value also includes `queryMode`, `queryTriples`, and `queryDerived`.
111
+ console.log(result.closureN3);
99
112
  ```
100
113
 
101
- > Note: the npm `reason()` helper shells out to the bundled `eyeling.js` CLI for simplicity and robustness.
114
+ #### `reasonStream()` output behavior
115
+
116
+ `closureN3` is also mode-dependent:
117
+
118
+ - **Normal mode:** by default, `closureN3` is the closure (**input facts + derived facts**)
119
+ - **`log:query` mode:** `closureN3` is the **query-selected triples**
120
+
121
+ To exclude input facts from the normal-mode closure, pass:
122
+
123
+ ```js
124
+ includeInputFactsInClosure: false;
125
+ ```
126
+
127
+ The returned object also includes `queryMode`, `queryTriples`, and `queryDerived` (and in normal mode, `onDerived` fires for newly derived facts; in `log:query` mode it fires for the query-selected derived triples).
102
128
 
103
129
  ## Builtins
104
130
 
105
- Builtins are defined in [eyeling-builtins.ttl](https://github.com/eyereasoner/eyeling/blob/main/eyeling-builtins.ttl) and described in the [HANDBOOK](https://eyereasoner.github.io/eyeling/HANDBOOK#ch11).
131
+ Builtins are defined in [eyeling-builtins.ttl](https://github.com/eyereasoner/eyeling/blob/main/eyeling-builtins.ttl) and described in the [Handbook (Chapter 11)](https://eyereasoner.github.io/eyeling/HANDBOOK#ch11).
106
132
 
107
- ## Testing (repo checkout)
133
+ ## Development and testing (repo checkout)
108
134
 
109
135
  ```bash
110
136
  npm test
111
137
  ```
112
138
 
139
+ You can also inspect the `examples/` directory for many small and large N3 programs.
140
+
113
141
  ## License
114
142
 
115
- MIT (see [LICENSE](https://github.com/eyereasoner/eyeling/blob/main/LICENSE.md)).
143
+ MIT see [LICENSE.md](https://github.com/eyereasoner/eyeling/blob/main/LICENSE.md).
@@ -0,0 +1,195 @@
1
+ # ===================================================================================
2
+ # odrl-dpv-campaign-audit.n3
3
+ #
4
+ # A "reasoning about reasoning" example using both ODRL and DPV.
5
+ #
6
+ # What this demonstrates
7
+ # ----------------------
8
+ # 1) Object-level reasoning inside a quoted policy theory:
9
+ # - ODRL permissions specify who may read which dataset
10
+ # - DPV annotations specify policy purpose and data category
11
+ # - local rules translate ODRL + DPV metadata into effective access summaries
12
+ #
13
+ # 2) Meta-level reasoning outside that quoted theory:
14
+ # - compute the closure of the quoted policy theory
15
+ # - inspect what the policy theory entails
16
+ # - derive an audit decision based on those entailed consequences
17
+ #
18
+ # Why this is "reasoning about reasoning"
19
+ # ---------------------------------------
20
+ # The final audit decision is derived from statements ABOUT the closure of the quoted
21
+ # policy theory (what the policy entails), not directly from the raw asserted policy.
22
+ #
23
+ # Modeling note
24
+ # -------------
25
+ # This is a pedagogical example:
26
+ # - ODRL is used for policy structure (permission/assignee/target/action)
27
+ # - DPV is used for privacy semantics (purpose, personal data categories)
28
+ # - local rules map the combination into audit-friendly facts
29
+ # ===================================================================================
30
+
31
+ @prefix : <http://example.org/#>.
32
+ @prefix ey: <https://eyereasoner.github.io/ns#>.
33
+ @prefix odrl: <http://www.w3.org/ns/odrl/2/>.
34
+ @prefix dpv: <http://www.w3.org/ns/dpv#>.
35
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
36
+
37
+ # -----------------------------------------------
38
+ # OBJECT THEORY (QUOTED ODRL + DPV POLICY THEORY)
39
+ # -----------------------------------------------
40
+
41
+ :case :policyTheory {
42
+ # ---- Request context (inside the quoted theory)
43
+ :ticket-99 :request :PublishCampaignAccessSummary.
44
+
45
+ # ---- Recipients (local classification for audit decisions)
46
+ :adVendor a :ExternalRecipient.
47
+ :marketingTeam a :InternalRecipient.
48
+
49
+ # ---- Datasets (annotated with DPV data category)
50
+ :AudienceList dpv:hasPersonalData dpv:PersonalData.
51
+ :ProductCatalog a :NonPersonalDataset.
52
+
53
+ # ---- ODRL policy A (benign-ish): internal team reads product catalog for service provision
54
+ :policy-internal a odrl:Set;
55
+ dpv:hasPurpose dpv:ServiceProvision;
56
+ odrl:permission [
57
+ odrl:assignee :marketingTeam;
58
+ odrl:target :ProductCatalog;
59
+ odrl:action odrl:read
60
+ ].
61
+
62
+ # ---- ODRL policy B (risky for publication): external vendor reads audience list for marketing
63
+ :policy-external a odrl:Set;
64
+ dpv:hasPurpose dpv:Marketing;
65
+ odrl:permission [
66
+ odrl:assignee :adVendor;
67
+ odrl:target :AudienceList;
68
+ odrl:action odrl:read
69
+ ].
70
+
71
+ # --------------------------------------------------------------------
72
+ # Object-level translation / normalization rules (pedagogical mapping)
73
+ # --------------------------------------------------------------------
74
+
75
+ # ODRL read permission -> effective read access
76
+ {
77
+ ?Policy odrl:permission [
78
+ odrl:assignee ?Recipient;
79
+ odrl:target ?Dataset;
80
+ odrl:action odrl:read
81
+ ].
82
+ }
83
+ =>
84
+ {
85
+ ?Recipient :canRead ?Dataset.
86
+ }.
87
+
88
+ # Lift policy purpose to the recipient (summary view used by the audit layer)
89
+ {
90
+ ?Policy dpv:hasPurpose ?Purpose.
91
+ ?Policy odrl:permission [
92
+ odrl:assignee ?Recipient;
93
+ odrl:target ?Dataset;
94
+ odrl:action odrl:read
95
+ ].
96
+ }
97
+ =>
98
+ {
99
+ ?Recipient :authorisedPurpose ?Purpose.
100
+ }.
101
+
102
+ # Combine ODRL permission target with DPV data-category annotation on the dataset
103
+ {
104
+ ?Policy odrl:permission [
105
+ odrl:assignee ?Recipient;
106
+ odrl:target ?Dataset;
107
+ odrl:action odrl:read
108
+ ].
109
+ ?Dataset dpv:hasPersonalData ?Category.
110
+ }
111
+ =>
112
+ {
113
+ ?Recipient :authorisedReadCategory ?Category.
114
+ }.
115
+ }.
116
+
117
+ # --------------------------
118
+ # META-LEVEL POLICY ANALYSIS
119
+ # --------------------------
120
+
121
+ # 1) Compute the closure of the quoted ODRL+DPV policy theory.
122
+ {
123
+ :case :policyTheory ?PolicyTheory.
124
+ ?PolicyTheory log:conclusion ?PolicyClosure.
125
+ }
126
+ =>
127
+ {
128
+ :case :policyClosure ?PolicyClosure.
129
+ }.
130
+
131
+ # 2) Record a witness if the policy closure entails:
132
+ # external recipient + marketing purpose + personal-data access.
133
+ #
134
+ # This is a statement ABOUT what the quoted theory entails.
135
+ {
136
+ :case :policyClosure ?C.
137
+ ?C log:includes {
138
+ ?Recipient a :ExternalRecipient.
139
+ ?Recipient :authorisedPurpose dpv:Marketing.
140
+ ?Recipient :authorisedReadCategory dpv:PersonalData.
141
+ }.
142
+ }
143
+ =>
144
+ {
145
+ :case :entails {
146
+ ?Recipient a :ExternalRecipient.
147
+ ?Recipient :authorisedPurpose dpv:Marketing.
148
+ ?Recipient :authorisedReadCategory dpv:PersonalData.
149
+ }.
150
+ :case :risk :ThirdPartyMarketingPersonalDataAccessEntailed.
151
+ }.
152
+
153
+ # 3) If the request is to publish the campaign access summary and the closure entails the
154
+ # risky pattern above, require manual review.
155
+ {
156
+ :case :policyTheory ?PolicyTheory.
157
+ ?PolicyTheory log:includes {
158
+ :ticket-99 :request :PublishCampaignAccessSummary.
159
+ }.
160
+ :case :risk :ThirdPartyMarketingPersonalDataAccessEntailed.
161
+ }
162
+ =>
163
+ {
164
+ :case :decision :ManualReviewRequired.
165
+ }.
166
+
167
+ # 4) Otherwise, if the same request is present but the closure does NOT entail any external
168
+ # recipient with marketing-purpose personal-data access, auto-approve.
169
+ #
170
+ # Important: log:notIncludes applies to FORMULAS, so it is applied to ?C (the closure).
171
+ {
172
+ :case :policyTheory ?PolicyTheory.
173
+ ?PolicyTheory log:includes {
174
+ :ticket-99 :request :PublishCampaignAccessSummary.
175
+ }.
176
+ :case :policyClosure ?C.
177
+ ?C log:notIncludes {
178
+ [] a :ExternalRecipient.
179
+ [] :authorisedPurpose dpv:Marketing.
180
+ [] :authorisedReadCategory dpv:PersonalData.
181
+ }.
182
+ }
183
+ =>
184
+ {
185
+ :case :decision :AutoApprove.
186
+ }.
187
+
188
+ # ------------
189
+ # QUERY INTENT
190
+ # ------------
191
+
192
+ # These top-level log:query directives define the query-projected outputs.
193
+ { :case :decision ?Decision. } log:query { :case :decision ?Decision. }.
194
+ { :case :entails ?Witness. } log:query { :case :entails ?Witness. }.
195
+
@@ -0,0 +1,264 @@
1
+ # =========================================================================================
2
+ # odrl-dpv-conflict-audit.n3
3
+ #
4
+ # A "reasoning about reasoning" example using ODRL + DPV with conflict detection.
5
+ #
6
+ # What this demonstrates
7
+ # ----------------------
8
+ # 1) Object-level reasoning inside a quoted policy theory:
9
+ # - ODRL permission/prohibition rules specify allowed/forbidden reads
10
+ # - DPV annotations specify purpose and personal-data involvement
11
+ # - local translation rules derive normalized access summaries
12
+ #
13
+ # 2) Meta-level reasoning outside that quoted theory:
14
+ # - compute the closure of the quoted theory
15
+ # - inspect what the quoted theory entails
16
+ # - detect a conflict pattern (permission + prohibition) in the closure
17
+ # - derive an audit decision from that detected conflict
18
+ #
19
+ # Why this is "reasoning about reasoning"
20
+ # ---------------------------------------
21
+ # The final audit decision is based on a pattern found in the closure of the quoted policy
22
+ # theory (i.e., what the policy *entails*), not merely on directly asserted source triples.
23
+ #
24
+ # Modeling note
25
+ # -------------
26
+ # This is a pedagogical example, not a full ODRL evaluator:
27
+ # - ODRL is used for policy structure (permission/prohibition/assignee/target/action)
28
+ # - DPV is used for purpose and personal-data semantics
29
+ # - local rules translate the combination into audit-friendly facts
30
+ # =========================================================================================
31
+
32
+ @prefix : <http://example.org/#>.
33
+ @prefix ey: <https://eyereasoner.github.io/ns#>.
34
+ @prefix odrl: <http://www.w3.org/ns/odrl/2/>.
35
+ @prefix dpv: <http://www.w3.org/ns/dpv#>.
36
+ @prefix pd: <https://w3id.org/dpv/pd#>.
37
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
38
+
39
+ # ------------------------------------------------------
40
+ # OBJECT THEORY (QUOTED ODRL + DPV POLICY / AUDIT INPUT)
41
+ # ------------------------------------------------------
42
+
43
+ :case :policyTheory {
44
+ # ---- Request context (inside the quoted theory)
45
+ :ticket-314 :request :PublishPartnerCampaignPolicy.
46
+
47
+ # ---- Local recipient classification used by audit reasoning
48
+ :adVendor a :ExternalRecipient.
49
+ :analyticsTeam a :InternalRecipient.
50
+
51
+ # ---- Dataset classification / DPV semantics
52
+ #
53
+ # We include both a concrete PD category and a generic PersonalData category so the
54
+ # example can query either level without depending on external DPV taxonomy inference.
55
+ :AudienceList dpv:hasPersonalData pd:EmailAddress.
56
+ :AudienceList dpv:hasPersonalData dpv:PersonalData.
57
+
58
+ :ProductCatalog a :NonPersonalDataset.
59
+
60
+ # ---- ODRL policy A: PERMIT external vendor to read audience list for marketing
61
+ :policy-allow-external a odrl:Set;
62
+ dpv:hasPurpose dpv:Marketing;
63
+ odrl:permission [
64
+ odrl:assignee :adVendor;
65
+ odrl:target :AudienceList;
66
+ odrl:action odrl:read
67
+ ].
68
+
69
+ # ---- ODRL policy B: PROHIBIT the same external vendor from reading the same dataset
70
+ # for the same purpose (intentional conflict for audit demonstration)
71
+ :policy-deny-external a odrl:Set;
72
+ dpv:hasPurpose dpv:Marketing;
73
+ odrl:prohibition [
74
+ odrl:assignee :adVendor;
75
+ odrl:target :AudienceList;
76
+ odrl:action odrl:read
77
+ ].
78
+
79
+ # ---- ODRL policy C: benign internal permission (noise / contrast)
80
+ :policy-allow-internal a odrl:Set;
81
+ dpv:hasPurpose dpv:ServiceProvision;
82
+ odrl:permission [
83
+ odrl:assignee :analyticsTeam;
84
+ odrl:target :ProductCatalog;
85
+ odrl:action odrl:read
86
+ ].
87
+
88
+ # ----------------------------------------------
89
+ # Object-level translation / normalization rules
90
+ # ----------------------------------------------
91
+
92
+ # Permission(read) -> normalized permission fact
93
+ {
94
+ ?Policy odrl:permission [
95
+ odrl:assignee ?Recipient;
96
+ odrl:target ?Dataset;
97
+ odrl:action odrl:read
98
+ ].
99
+ }
100
+ =>
101
+ {
102
+ ?Recipient :permittedRead ?Dataset.
103
+ }.
104
+
105
+ # Prohibition(read) -> normalized prohibition fact
106
+ {
107
+ ?Policy odrl:prohibition [
108
+ odrl:assignee ?Recipient;
109
+ odrl:target ?Dataset;
110
+ odrl:action odrl:read
111
+ ].
112
+ }
113
+ =>
114
+ {
115
+ ?Recipient :prohibitedRead ?Dataset.
116
+ }.
117
+
118
+ # Lift policy purpose to recipient (permission side)
119
+ {
120
+ ?Policy dpv:hasPurpose ?Purpose.
121
+ ?Policy odrl:permission [
122
+ odrl:assignee ?Recipient;
123
+ odrl:target ?Dataset;
124
+ odrl:action odrl:read
125
+ ].
126
+ }
127
+ =>
128
+ {
129
+ ?Recipient :permittedPurpose ?Purpose.
130
+ }.
131
+
132
+ # Lift policy purpose to recipient (prohibition side)
133
+ {
134
+ ?Policy dpv:hasPurpose ?Purpose.
135
+ ?Policy odrl:prohibition [
136
+ odrl:assignee ?Recipient;
137
+ odrl:target ?Dataset;
138
+ odrl:action odrl:read
139
+ ].
140
+ }
141
+ =>
142
+ {
143
+ ?Recipient :prohibitedPurpose ?Purpose.
144
+ }.
145
+
146
+ # Combine permission target with DPV personal-data category annotation
147
+ {
148
+ ?Policy odrl:permission [
149
+ odrl:assignee ?Recipient;
150
+ odrl:target ?Dataset;
151
+ odrl:action odrl:read
152
+ ].
153
+ ?Dataset dpv:hasPersonalData ?Category.
154
+ }
155
+ =>
156
+ {
157
+ ?Recipient :permittedReadCategory ?Category.
158
+ }.
159
+
160
+ # Combine prohibition target with DPV personal-data category annotation
161
+ {
162
+ ?Policy odrl:prohibition [
163
+ odrl:assignee ?Recipient;
164
+ odrl:target ?Dataset;
165
+ odrl:action odrl:read
166
+ ].
167
+ ?Dataset dpv:hasPersonalData ?Category.
168
+ }
169
+ =>
170
+ {
171
+ ?Recipient :prohibitedReadCategory ?Category.
172
+ }.
173
+ }.
174
+
175
+ # --------------------------
176
+ # META-LEVEL POLICY ANALYSIS
177
+ # --------------------------
178
+
179
+ # 1) Compute the closure of the quoted ODRL+DPV policy theory.
180
+ {
181
+ :case :policyTheory ?PolicyTheory.
182
+ ?PolicyTheory log:conclusion ?PolicyClosure.
183
+ }
184
+ =>
185
+ {
186
+ :case :policyClosure ?PolicyClosure.
187
+ }.
188
+
189
+ # 2) Detect a conflict witness by inspecting the policy closure.
190
+ #
191
+ # Conflict pattern:
192
+ # - same external recipient
193
+ # - marketing purpose permitted AND prohibited
194
+ # - personal-data access category permitted AND prohibited
195
+ #
196
+ # This rule does NOT inspect raw asserted policy statements directly;
197
+ # it inspects normalized consequences entailed by the quoted theory's closure.
198
+ {
199
+ :case :policyClosure ?C.
200
+ ?C log:includes {
201
+ ?Recipient a :ExternalRecipient.
202
+ ?Recipient :permittedPurpose dpv:Marketing.
203
+ ?Recipient :prohibitedPurpose dpv:Marketing.
204
+ ?Recipient :permittedReadCategory dpv:PersonalData.
205
+ ?Recipient :prohibitedReadCategory dpv:PersonalData.
206
+ }.
207
+ }
208
+ =>
209
+ {
210
+ :case :entails {
211
+ ?Recipient a :ExternalRecipient.
212
+ ?Recipient :permittedPurpose dpv:Marketing.
213
+ ?Recipient :prohibitedPurpose dpv:Marketing.
214
+ ?Recipient :permittedReadCategory dpv:PersonalData.
215
+ ?Recipient :prohibitedReadCategory dpv:PersonalData.
216
+ }.
217
+ :case :risk :PermissionProhibitionConflictEntailed.
218
+ }.
219
+
220
+ # 3) If the request is to publish the partner campaign policy and a conflict is entailed,
221
+ # require manual review.
222
+ {
223
+ :case :policyTheory ?PolicyTheory.
224
+ ?PolicyTheory log:includes {
225
+ :ticket-314 :request :PublishPartnerCampaignPolicy.
226
+ }.
227
+ :case :risk :PermissionProhibitionConflictEntailed.
228
+ }
229
+ =>
230
+ {
231
+ :case :decision :ManualReviewRequired.
232
+ }.
233
+
234
+ # 4) Otherwise, if the request is present but the closure does NOT entail the conflict
235
+ # pattern, auto-approve.
236
+ #
237
+ # Important: log:notIncludes applies to FORMULAS, so it is applied to ?C (the closure).
238
+ {
239
+ :case :policyTheory ?PolicyTheory.
240
+ ?PolicyTheory log:includes {
241
+ :ticket-314 :request :PublishPartnerCampaignPolicy.
242
+ }.
243
+ :case :policyClosure ?C.
244
+ ?C log:notIncludes {
245
+ [] a :ExternalRecipient.
246
+ [] :permittedPurpose dpv:Marketing.
247
+ [] :prohibitedPurpose dpv:Marketing.
248
+ [] :permittedReadCategory dpv:PersonalData.
249
+ [] :prohibitedReadCategory dpv:PersonalData.
250
+ }.
251
+ }
252
+ =>
253
+ {
254
+ :case :decision :AutoApprove.
255
+ }.
256
+
257
+ # ------------
258
+ # QUERY INTENT
259
+ # ------------
260
+
261
+ # Top-level query directives define what is printed in query mode.
262
+ { :case :decision ?Decision. } log:query { :case :decision ?Decision. }.
263
+ { :case :entails ?Witness. } log:query { :case :entails ?Witness. }.
264
+
@@ -0,0 +1,147 @@
1
+ # ==========================================================================================
2
+ # odrl-policy-audit.n3
3
+ #
4
+ # A "reasoning about reasoning" example using ODRL policies.
5
+ #
6
+ # What this demonstrates
7
+ # ----------------------
8
+ # 1) Object-level reasoning (inside a quoted ODRL policy theory):
9
+ # - ODRL permissions (assignee/target/action) are translated into effective access facts
10
+ #
11
+ # 2) Meta-level reasoning (outside that quoted theory):
12
+ # - compute the closure of the quoted policy theory
13
+ # - inspect what the policy theory entails
14
+ # - derive an audit decision based on entailed consequences
15
+ #
16
+ # Why this is "reasoning about reasoning"
17
+ # ---------------------------------------
18
+ # The audit decision is not derived directly from the asserted ODRL policy statements.
19
+ # It is derived from statements ABOUT what the quoted ODRL policy theory entails
20
+ # (i.e., from the closure of that quoted theory).
21
+ # ==========================================================================================
22
+
23
+ @prefix : <http://example.org/#>.
24
+ @prefix ey: <https://eyereasoner.github.io/ns#>.
25
+ @prefix odrl: <http://www.w3.org/ns/odrl/2/>.
26
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
27
+
28
+ # ----------------------------------
29
+ # OBJECT THEORY (QUOTED ODRL POLICY)
30
+ # ----------------------------------
31
+
32
+ # The ODRL policy and object-level translation rules live inside a quoted formula so that
33
+ # the surrounding (meta-level) rules can reason about its semantics (its closure) as data.
34
+ :case :policyTheory {
35
+ # ---- Context / request (asserted input within the quoted theory)
36
+ :ticket-42 :request :PublishPolicySnapshot.
37
+
38
+ # ---- ODRL policy statements
39
+ #
40
+ # This policy grants Alice permission to read two assets:
41
+ # - a general handbook (benign)
42
+ # - the payroll ledger (sensitive for publication audits)
43
+ :policy-1 a odrl:Set;
44
+ odrl:permission [
45
+ odrl:assignee :alice;
46
+ odrl:target :EmployeeHandbook;
47
+ odrl:action odrl:read
48
+ ];
49
+ odrl:permission [
50
+ odrl:assignee :alice;
51
+ odrl:target :PayrollLedger;
52
+ odrl:action odrl:read
53
+ ].
54
+
55
+ # ---- Domain classification
56
+ :PayrollLedger a :SensitiveAsset.
57
+ :PayrollLedger a :PayrollAsset.
58
+ :EmployeeHandbook a :PublicAsset.
59
+
60
+ # ---- Object-level reasoning rules over ODRL permissions
61
+ #
62
+ # Translate a simple ODRL permission statement into an effective access fact.
63
+ # (This is a minimal pedagogical mapping, not a complete ODRL evaluator.)
64
+ {
65
+ ?Policy odrl:permission [
66
+ odrl:assignee ?User;
67
+ odrl:target ?Asset;
68
+ odrl:action odrl:read
69
+ ].
70
+ }
71
+ =>
72
+ {
73
+ ?User :canRead ?Asset.
74
+ }.
75
+
76
+ # Normalize "canRead" into a generic access predicate used by the audit layer.
77
+ {
78
+ ?User :canRead ?Asset.
79
+ }
80
+ =>
81
+ {
82
+ ?User :canAccess ?Asset.
83
+ }.
84
+ }.
85
+
86
+ # --------------------------
87
+ # META-LEVEL POLICY ANALYSIS
88
+ # --------------------------
89
+
90
+ # 1) Compute the closure of the quoted ODRL policy theory.
91
+ {
92
+ :case :policyTheory ?PolicyTheory.
93
+ ?PolicyTheory log:conclusion ?PolicyClosure.
94
+ }
95
+ =>
96
+ {
97
+ :case :policyClosure ?PolicyClosure.
98
+ }.
99
+
100
+ # 2) Record a witness if the quoted policy theory entails access to the payroll ledger.
101
+ # This is a statement ABOUT what the policy theory entails.
102
+ {
103
+ :case :policyClosure ?C.
104
+ ?C log:includes { ?User :canAccess :PayrollLedger. }.
105
+ }
106
+ =>
107
+ {
108
+ :case :entails { ?User :canAccess :PayrollLedger. }.
109
+ :case :risk :SensitiveAccessEntailed.
110
+ }.
111
+
112
+ # 3) If a publication request is present in the quoted theory AND sensitive access is
113
+ # entailed by the quoted theory, require manual review.
114
+ {
115
+ :case :policyTheory ?PolicyTheory.
116
+ ?PolicyTheory log:includes { :ticket-42 :request :PublishPolicySnapshot. }.
117
+ :case :risk :SensitiveAccessEntailed.
118
+ }
119
+ =>
120
+ {
121
+ :case :decision :ManualReviewRequired.
122
+ }.
123
+
124
+ # 4) Otherwise, if the publication request is present but NO payroll-ledger access is
125
+ # entailed by the policy closure, the case can be auto-approved.
126
+ #
127
+ # Important: log:notIncludes applies to FORMULAS, so we test absence on ?C (the closure),
128
+ # not on a regular resource like :case.
129
+ {
130
+ :case :policyTheory ?PolicyTheory.
131
+ ?PolicyTheory log:includes { :ticket-42 :request :PublishPolicySnapshot. }.
132
+ :case :policyClosure ?C.
133
+ ?C log:notIncludes { _:user :canAccess :PayrollLedger. }.
134
+ }
135
+ =>
136
+ {
137
+ :case :decision :AutoApprove.
138
+ }.
139
+
140
+ # ------------
141
+ # QUERY INTENT
142
+ # ------------
143
+
144
+ # These top-level log:query statements describe the intended outputs.
145
+ { :case :decision ?Decision. } log:query { :case :decision ?Decision. }.
146
+ { :case :entails ?Witness. } log:query { :case :entails ?Witness. }.
147
+
@@ -0,0 +1,10 @@
1
+ @prefix : <http://example.org/#> .
2
+ @prefix dpv: <http://www.w3.org/ns/dpv#> .
3
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
4
+
5
+ :case :decision :ManualReviewRequired .
6
+ :case :entails {
7
+ :adVendor a :ExternalRecipient .
8
+ :adVendor :authorisedPurpose dpv:Marketing .
9
+ :adVendor :authorisedReadCategory dpv:PersonalData .
10
+ } .
@@ -0,0 +1,12 @@
1
+ @prefix : <http://example.org/#> .
2
+ @prefix dpv: <http://www.w3.org/ns/dpv#> .
3
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
4
+
5
+ :case :decision :ManualReviewRequired .
6
+ :case :entails {
7
+ :adVendor a :ExternalRecipient .
8
+ :adVendor :permittedPurpose dpv:Marketing .
9
+ :adVendor :prohibitedPurpose dpv:Marketing .
10
+ :adVendor :permittedReadCategory dpv:PersonalData .
11
+ :adVendor :prohibitedReadCategory dpv:PersonalData .
12
+ } .
@@ -0,0 +1,6 @@
1
+ @prefix : <http://example.org/#> .
2
+
3
+ :case :decision :ManualReviewRequired .
4
+ :case :entails {
5
+ :alice :canAccess :PayrollLedger .
6
+ } .
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.13.5",
3
+ "version": "1.13.7",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -30,17 +30,6 @@ function run(cmd, args, opts = {}) {
30
30
  });
31
31
  }
32
32
 
33
- function hasGit() {
34
- const r = run('git', ['--version']);
35
- return r.status === 0;
36
- }
37
-
38
- function inGitWorktree(cwd) {
39
- if (!hasGit()) return false;
40
- const r = run('git', ['rev-parse', '--is-inside-work-tree'], { cwd });
41
- return r.status === 0 && String(r.stdout).trim() === 'true';
42
- }
43
-
44
33
  // Normalize output for comparison.
45
34
  // Eyeling (and other N3 tools) may emit the same closure with different
46
35
  // triple ordering. Examples tests should verify content, not presentation.
@@ -83,26 +72,10 @@ function rmrf(p) {
83
72
  } catch {}
84
73
  }
85
74
 
86
- function showDiff({ IN_GIT, examplesDir, expectedPath, generatedPath, relExpectedPosix }) {
87
- if (hasGit()) {
88
- if (IN_GIT) {
89
- // Show repo diff for the overwritten golden file
90
- const d = run('git', ['diff', '--', relExpectedPosix], { cwd: examplesDir });
91
- if (d.stdout) process.stdout.write(d.stdout);
92
- if (d.stderr) process.stderr.write(d.stderr);
93
- } else {
94
- // Show no-index diff between packaged golden and generated tmp
95
- const d = run('git', ['diff', '--no-index', expectedPath, generatedPath], { cwd: examplesDir });
96
- // Replace tmp path in output (nice UX)
97
- if (d.stdout) process.stdout.write(String(d.stdout).replaceAll(generatedPath, 'generated'));
98
- if (d.stderr) process.stderr.write(String(d.stderr).replaceAll(generatedPath, 'generated'));
99
- }
100
- } else {
101
- // Fallback: diff -u
102
- const d = run('diff', ['-u', expectedPath, generatedPath], { cwd: examplesDir });
103
- if (d.stdout) process.stdout.write(d.stdout);
104
- if (d.stderr) process.stderr.write(d.stderr);
105
- }
75
+ function showDiff({ examplesDir, expectedPath, generatedPath }) {
76
+ const d = run('diff', ['-u', expectedPath, generatedPath], { cwd: examplesDir });
77
+ if (d.stdout) process.stdout.write(d.stdout);
78
+ if (d.stderr) process.stderr.write(d.stderr);
106
79
  }
107
80
 
108
81
  function main() {
@@ -124,14 +97,12 @@ function main() {
124
97
  process.exit(1);
125
98
  }
126
99
 
127
- const IN_GIT = inGitWorktree(root);
128
-
129
100
  const files = fs
130
101
  .readdirSync(examplesDir)
131
102
  .filter((f) => f.endsWith('.n3'))
132
103
  .sort((a, b) => a.localeCompare(b));
133
104
 
134
- info(`Running ${files.length} examples tests (${IN_GIT ? 'git worktree mode' : 'npm-installed mode'})`);
105
+ info(`Running ${files.length} examples tests`);
135
106
  console.log(`${C.dim}${getEyelingVersion(nodePath, eyelingJsPath, root)}; node ${process.version}${C.n}`);
136
107
 
137
108
  if (files.length === 0) {
@@ -139,9 +110,6 @@ function main() {
139
110
  process.exit(0);
140
111
  }
141
112
 
142
- // In maintainer mode we overwrite tracked goldens in examples/output/
143
- if (IN_GIT) fs.mkdirSync(outputDir, { recursive: true });
144
-
145
113
  let passed = 0;
146
114
  let failed = 0;
147
115
 
@@ -156,7 +124,6 @@ function main() {
156
124
 
157
125
  const filePath = path.join(examplesDir, file);
158
126
  const expectedPath = path.join(outputDir, file);
159
- const relExpectedPosix = path.posix.join('output', file); // for git diff inside examplesDir
160
127
 
161
128
  let n3Text;
162
129
  try {
@@ -171,34 +138,19 @@ function main() {
171
138
 
172
139
  const expectedRc = expectedExitCode(n3Text);
173
140
 
174
- // Snapshot expected output before we potentially overwrite it in git worktree mode.
175
- // This lets us compare content ignoring ordering, while still allowing the script
176
- // to regenerate output/ files in-place when working from a repo checkout.
177
- let expectedBeforeText = null;
178
- if (IN_GIT && fs.existsSync(expectedPath)) {
179
- try {
180
- expectedBeforeText = fs.readFileSync(expectedPath, 'utf8');
181
- } catch {
182
- expectedBeforeText = null;
183
- }
141
+ // Always write generated output to a temp file. This avoids mutating tracked
142
+ // examples/output/* during normal test runs and makes timing behavior more
143
+ // comparable across environments.
144
+ if (!fs.existsSync(expectedPath)) {
145
+ const ms = Date.now() - start;
146
+ fail(`${idx} ${file} ${msTag(ms)}`);
147
+ fail(`Missing expected output/${file}`);
148
+ failed++;
149
+ continue;
184
150
  }
185
151
 
186
- // Decide where generated output goes
187
- let tmpDir = null;
188
- let generatedPath = expectedPath;
189
-
190
- if (!IN_GIT) {
191
- // npm-installed / no .git: never modify output/ in node_modules
192
- if (!fs.existsSync(expectedPath)) {
193
- const ms = Date.now() - start;
194
- fail(`${idx} ${file} ${msTag(ms)}`);
195
- fail(`Missing expected output/${file}`);
196
- failed++;
197
- continue;
198
- }
199
- tmpDir = mkTmpDir();
200
- generatedPath = path.join(tmpDir, 'generated.n3');
201
- }
152
+ const tmpDir = mkTmpDir();
153
+ const generatedPath = path.join(tmpDir, 'generated.n3');
202
154
 
203
155
  // Run eyeling on this file (cwd examplesDir so relative behavior matches old script)
204
156
  const outFd = fs.openSync(generatedPath, 'w');
@@ -219,7 +171,7 @@ function main() {
219
171
  // Compare output (order-insensitive)
220
172
  let diffOk = false;
221
173
  try {
222
- const expectedText = IN_GIT ? expectedBeforeText : fs.readFileSync(expectedPath, 'utf8');
174
+ const expectedText = fs.readFileSync(expectedPath, 'utf8');
223
175
  const generatedText = fs.readFileSync(generatedPath, 'utf8');
224
176
  if (expectedText == null) throw new Error('missing expected output');
225
177
  diffOk = normalizeForCompare(expectedText) === normalizeForCompare(generatedText);
@@ -245,13 +197,11 @@ function main() {
245
197
  fail('Output differs');
246
198
  }
247
199
 
248
- // Show diffs (both modes), because this is a test runner
200
+ // Show diffs, because this is a test runner
249
201
  showDiff({
250
- IN_GIT,
251
202
  examplesDir,
252
203
  expectedPath,
253
204
  generatedPath,
254
- relExpectedPosix,
255
205
  });
256
206
 
257
207
  failed++;
@@ -2,14 +2,9 @@
2
2
  'use strict';
3
3
 
4
4
  // Convert examples/input/*.{ttl,trig} -> examples/*.n3 using n3gen.js
5
- // Designed to work both in a git checkout (maintainer mode) and in an npm-installed package.
6
5
  //
7
- // In git mode:
8
- // - overwrites examples/<name>.n3
9
- // - uses `git diff` to validate + show diffs
10
- // In non-git mode:
11
- // - writes to a temp dir
12
- // - compares against packaged examples/<name>.n3 without modifying it
6
+ // For reproducibility and to avoid mutating tracked files during tests, generated output
7
+ // is always written to a temporary file and compared against examples/<name>.n3.
13
8
 
14
9
  const fs = require('node:fs');
15
10
  const os = require('node:os');
@@ -39,23 +34,6 @@ function run(cmd, args, opts = {}) {
39
34
  });
40
35
  }
41
36
 
42
- function hasGit() {
43
- const r = run('git', ['--version']);
44
- return r.status === 0;
45
- }
46
-
47
- function inGitWorktree(cwd) {
48
- if (!hasGit()) return false;
49
- const r = run('git', ['rev-parse', '--is-inside-work-tree'], { cwd });
50
- return r.status === 0 && String(r.stdout).trim() === 'true';
51
- }
52
-
53
- function isTracked(cwd, relPathPosix) {
54
- if (!hasGit()) return false;
55
- const r = run('git', ['ls-files', '--error-unmatch', relPathPosix], { cwd });
56
- return r.status === 0;
57
- }
58
-
59
37
  function mkTmpDir() {
60
38
  return fs.mkdtempSync(path.join(os.tmpdir(), 'eyeling-n3-'));
61
39
  }
@@ -66,29 +44,10 @@ function rmrf(p) {
66
44
  } catch {}
67
45
  }
68
46
 
69
- function showDiff({ IN_GIT, examplesDir, expectedPath, generatedPath, relExpectedPosix }) {
70
- if (hasGit()) {
71
- if (IN_GIT) {
72
- // If tracked: show repo diff; if untracked: show addition via no-index diff against /dev/null.
73
- if (isTracked(examplesDir, relExpectedPosix)) {
74
- const d = run('git', ['diff', '--', relExpectedPosix], { cwd: examplesDir });
75
- if (d.stdout) process.stdout.write(d.stdout);
76
- if (d.stderr) process.stderr.write(d.stderr);
77
- } else {
78
- const d = run('git', ['diff', '--no-index', '--', '/dev/null', expectedPath], { cwd: examplesDir });
79
- if (d.stdout) process.stdout.write(String(d.stdout).replaceAll(expectedPath, relExpectedPosix));
80
- if (d.stderr) process.stderr.write(String(d.stderr).replaceAll(expectedPath, relExpectedPosix));
81
- }
82
- } else {
83
- const d = run('git', ['diff', '--no-index', expectedPath, generatedPath], { cwd: examplesDir });
84
- if (d.stdout) process.stdout.write(String(d.stdout).replaceAll(generatedPath, 'generated'));
85
- if (d.stderr) process.stderr.write(String(d.stderr).replaceAll(generatedPath, 'generated'));
86
- }
87
- } else {
88
- const d = run('diff', ['-u', expectedPath, generatedPath], { cwd: examplesDir });
89
- if (d.stdout) process.stdout.write(d.stdout);
90
- if (d.stderr) process.stderr.write(d.stderr);
91
- }
47
+ function showDiff({ examplesDir, expectedPath, generatedPath }) {
48
+ const d = run('diff', ['-u', expectedPath, generatedPath], { cwd: examplesDir });
49
+ if (d.stdout) process.stdout.write(d.stdout);
50
+ if (d.stderr) process.stderr.write(d.stderr);
92
51
  }
93
52
 
94
53
  function main() {
@@ -114,14 +73,12 @@ function main() {
114
73
  process.exit(1);
115
74
  }
116
75
 
117
- const IN_GIT = inGitWorktree(root);
118
-
119
76
  const inputs = fs
120
77
  .readdirSync(inputDir)
121
78
  .filter((f) => /\.(ttl|trig)$/i.test(f))
122
79
  .sort((a, b) => a.localeCompare(b));
123
80
 
124
- info(`Running n3 conversions for ${inputs.length} inputs (${IN_GIT ? 'git worktree mode' : 'npm-installed mode'})`);
81
+ info(`Running n3 conversions for ${inputs.length} inputs`);
125
82
  console.log(`${C.dim}node ${process.version}${C.n}`);
126
83
 
127
84
  if (inputs.length === 0) {
@@ -142,23 +99,18 @@ function main() {
142
99
  const outFile = `${base}.n3`;
143
100
 
144
101
  const expectedPath = path.join(examplesDir, outFile);
145
- const relExpectedPosix = outFile; // relative to examplesDir
146
-
147
- let tmpDir = null;
148
- let generatedPath = expectedPath;
149
-
150
- if (!IN_GIT) {
151
- if (!fs.existsSync(expectedPath)) {
152
- const ms = Date.now() - start;
153
- fail(`${idx} ${inFile} -> ${outFile} (${ms} ms)`);
154
- fail(`Missing expected examples/${outFile}`);
155
- failed++;
156
- continue;
157
- }
158
- tmpDir = mkTmpDir();
159
- generatedPath = path.join(tmpDir, outFile);
102
+
103
+ if (!fs.existsSync(expectedPath)) {
104
+ const ms = Date.now() - start;
105
+ fail(`${idx} ${inFile} -> ${outFile} (${ms} ms)`);
106
+ fail(`Missing expected examples/${outFile}`);
107
+ failed++;
108
+ continue;
160
109
  }
161
110
 
111
+ const tmpDir = mkTmpDir();
112
+ const generatedPath = path.join(tmpDir, outFile);
113
+
162
114
  // Run converter (stdout -> file; stderr captured)
163
115
  const outFd = fs.openSync(generatedPath, 'w');
164
116
  const r = cp.spawnSync(nodePath, [n3GenJsPath, inPath], {
@@ -177,29 +129,13 @@ function main() {
177
129
  fail(`Converter exit code ${rc}`);
178
130
  if (r.stderr) process.stderr.write(String(r.stderr));
179
131
  failed++;
180
- if (tmpDir) rmrf(tmpDir);
132
+ rmrf(tmpDir);
181
133
  continue;
182
134
  }
183
135
 
184
- // Compare output
185
- let diffOk = false;
186
- if (IN_GIT) {
187
- if (isTracked(examplesDir, relExpectedPosix)) {
188
- const d = run('git', ['diff', '--quiet', '--', relExpectedPosix], { cwd: examplesDir });
189
- diffOk = d.status === 0;
190
- } else {
191
- // Untracked file counts as a diff (work to do)
192
- diffOk = false;
193
- }
194
- } else {
195
- if (hasGit()) {
196
- const d = run('git', ['diff', '--no-index', '--quiet', expectedPath, generatedPath], { cwd: examplesDir });
197
- diffOk = d.status === 0;
198
- } else {
199
- const d = run('diff', ['-u', expectedPath, generatedPath], { cwd: examplesDir });
200
- diffOk = d.status === 0;
201
- }
202
- }
136
+ // Compare output (always compare expected vs generated temp file)
137
+ const d = run('diff', ['-u', expectedPath, generatedPath], { cwd: examplesDir });
138
+ const diffOk = d.status === 0;
203
139
 
204
140
  if (diffOk) {
205
141
  ok(`${idx} ${inFile} -> ${outFile} (${ms} ms)`);
@@ -207,11 +143,11 @@ function main() {
207
143
  } else {
208
144
  fail(`${idx} ${inFile} -> ${outFile} (${ms} ms)`);
209
145
  fail('Output differs');
210
- showDiff({ IN_GIT, examplesDir, expectedPath, generatedPath, relExpectedPosix });
146
+ showDiff({ examplesDir, expectedPath, generatedPath });
211
147
  failed++;
212
148
  }
213
149
 
214
- if (tmpDir) rmrf(tmpDir);
150
+ rmrf(tmpDir);
215
151
  }
216
152
 
217
153
  console.log('');