eyeling 1.13.6 → 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 +53 -25
- package/examples/odrl-dpv-campaign-audit.n3 +195 -0
- package/examples/odrl-dpv-conflict-audit.n3 +264 -0
- package/examples/odrl-policy-audit.n3 +147 -0
- package/examples/output/odrl-dpv-campaign-audit.n3 +10 -0
- package/examples/output/odrl-dpv-conflict-audit.n3 +12 -0
- package/examples/output/odrl-policy-audit.n3 +6 -0
- package/package.json +1 -1
- package/test/examples.test.js +18 -68
- package/test/n3gen.test.js +23 -87
package/README.md
CHANGED
|
@@ -2,9 +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
|
|
6
|
-
- Forward (`=>`)
|
|
7
|
-
- **CLI / npm `reason()` output is mode-dependent by default**: **newly derived forward facts** in normal mode, or
|
|
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
|
|
8
8
|
- Works in Node.js and fully client-side (browser/worker)
|
|
9
9
|
|
|
10
10
|
## Links
|
|
@@ -15,9 +15,7 @@ A compact [Notation3 (N3)](https://notation3.org/) reasoner in **JavaScript**.
|
|
|
15
15
|
- **Notation3 test suite:** [https://codeberg.org/phochste/notation3tests](https://codeberg.org/phochste/notation3tests)
|
|
16
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)
|
|
17
17
|
|
|
18
|
-
Eyeling is regularly checked against the community Notation3 test suite
|
|
19
|
-
|
|
20
|
-
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.
|
|
21
19
|
|
|
22
20
|
## Quick start
|
|
23
21
|
|
|
@@ -31,7 +29,7 @@ If you want to understand how the parser, unifier, proof search, skolemization,
|
|
|
31
29
|
npm i eyeling
|
|
32
30
|
```
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
## CLI usage
|
|
35
33
|
|
|
36
34
|
Run on a file:
|
|
37
35
|
|
|
@@ -39,23 +37,33 @@ Run on a file:
|
|
|
39
37
|
npx eyeling examples/socrates.n3
|
|
40
38
|
```
|
|
41
39
|
|
|
42
|
-
|
|
40
|
+
Show all options:
|
|
43
41
|
|
|
44
42
|
```bash
|
|
45
43
|
npx eyeling --help
|
|
46
44
|
```
|
|
47
45
|
|
|
48
|
-
|
|
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)
|
|
49
55
|
|
|
50
|
-
If
|
|
56
|
+
If the input contains one or more **top-level** directives of the form:
|
|
51
57
|
|
|
52
58
|
```n3
|
|
53
59
|
{ ?x a :Human. } log:query { ?x a :Mortal. }.
|
|
54
60
|
```
|
|
55
61
|
|
|
56
|
-
Eyeling
|
|
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).
|
|
57
63
|
|
|
58
|
-
|
|
64
|
+
## JavaScript API
|
|
65
|
+
|
|
66
|
+
### npm helper: `reason()`
|
|
59
67
|
|
|
60
68
|
CommonJS:
|
|
61
69
|
|
|
@@ -69,7 +77,7 @@ const input = `
|
|
|
69
77
|
:Socrates a :Human.
|
|
70
78
|
:Human rdfs:subClassOf :Mortal.
|
|
71
79
|
|
|
72
|
-
{ ?
|
|
80
|
+
{ ?s a ?A. ?A rdfs:subClassOf ?B. } => { ?s a ?B. }.
|
|
73
81
|
`;
|
|
74
82
|
|
|
75
83
|
console.log(reason({ proofComments: false }, input));
|
|
@@ -79,37 +87,57 @@ ESM:
|
|
|
79
87
|
|
|
80
88
|
```js
|
|
81
89
|
import eyeling from 'eyeling';
|
|
90
|
+
|
|
82
91
|
console.log(eyeling.reason({ proofComments: false }, input));
|
|
83
92
|
```
|
|
84
93
|
|
|
85
|
-
|
|
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`):
|
|
86
103
|
|
|
87
104
|
```js
|
|
88
|
-
const
|
|
105
|
+
const result = eyeling.reasonStream(input, {
|
|
89
106
|
proof: false,
|
|
90
107
|
onDerived: ({ triple }) => console.log(triple),
|
|
108
|
+
// includeInputFactsInClosure: false,
|
|
91
109
|
});
|
|
92
110
|
|
|
93
|
-
|
|
94
|
-
// - normal forward mode: closure (input facts + derived facts) by default
|
|
95
|
-
// - `log:query` mode: the query-selected triples
|
|
96
|
-
// To exclude input facts from the normal-mode closure, pass:
|
|
97
|
-
// includeInputFactsInClosure: false
|
|
98
|
-
// The return value also includes `queryMode`, `queryTriples`, and `queryDerived`.
|
|
111
|
+
console.log(result.closureN3);
|
|
99
112
|
```
|
|
100
113
|
|
|
101
|
-
|
|
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 [
|
|
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
|
-
##
|
|
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
|
|
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
|
+
} .
|
package/package.json
CHANGED
package/test/examples.test.js
CHANGED
|
@@ -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({
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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 =
|
|
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
|
|
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++;
|
package/test/n3gen.test.js
CHANGED
|
@@ -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
|
-
//
|
|
8
|
-
//
|
|
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({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
132
|
+
rmrf(tmpDir);
|
|
181
133
|
continue;
|
|
182
134
|
}
|
|
183
135
|
|
|
184
|
-
// Compare output
|
|
185
|
-
|
|
186
|
-
|
|
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({
|
|
146
|
+
showDiff({ examplesDir, expectedPath, generatedPath });
|
|
211
147
|
failed++;
|
|
212
148
|
}
|
|
213
149
|
|
|
214
|
-
|
|
150
|
+
rmrf(tmpDir);
|
|
215
151
|
}
|
|
216
152
|
|
|
217
153
|
console.log('');
|