@githonllc/avouch 0.1.9 → 0.1.11
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/SPEC.md +13 -13
- package/dist/src/checker.js +21 -6
- package/dist/src/contract.d.ts +1 -1
- package/dist/src/contract.js +2 -2
- package/dist/src/evaluate.js +3 -1
- package/dist/src/expr.js +3 -1
- package/dist/src/query.d.ts +1 -0
- package/dist/src/query.js +1 -0
- package/docs/adapter-guide.md +1 -1
- package/ontology.schema.json +13 -8
- package/package.json +1 -1
- package/vocab.ttl +7 -2
package/SPEC.md
CHANGED
|
@@ -57,8 +57,8 @@ The definitions quoted in the "Palantir analog" column are short quotations from
|
|
|
57
57
|
| `derivedProperties.<d>.expr` / `cite` / `type` | foundation | derived property definition | the definition as an expression over `self` (instead of `reads`), its source text and its declared type; a predicate expression has type `boolean`; without `expr` the derived property is undefined for evaluation; R1, R2, R4, R12 |
|
|
58
58
|
| `linkTypes` | foundation | link type: "the schema definition of a relationship between two object types" | `via` / `table` map to a foreign key or a many-to-many datasource; `create_rule` is an extension (R5) |
|
|
59
59
|
| `actionTypes` | foundation | action type: "the schema definition of a set of changes or edits to objects, property values, and links that a user can take at once" | |
|
|
60
|
-
| `actionTypes.<A>.parameters` | foundation | action parameter: "Parameters are the inputs of an action type." | name → `{cite, type?, optional?, values?, projectedFrom?}`; read by a condition as `{param}` or, in an expression, `{ref}`; `type` is needed when an expression reads it; `optional: true` lets the caller omit it; `values` (only with `type: enum`) is the value domain of the parameter; `projectedFrom` lists the request fields that the project resolves to this ref; it takes effect only on a `type: {ref: <Object>}` parameter with at least one field, and such a projected parameter
|
|
61
|
-
| `conditions` | foundation + constraint | submission criteria: "the conditions that determine whether an action can be submitted"; Palantir submission criteria ≈ the rows whose disposition class is `rejected` | named Boolean claims `{id, expr \| unspecified, reads?, reads_unspecified?, cites[{cite}], scenarios?}`: `expr` is the claim as an expression (section 9); `unspecified` says it is not written as one, and `reads` may then list what it reads (properties, parameters `{param, values?}`, the caller `{actor: id}` / `{actor: keys}`); R1, R4, R12 |
|
|
60
|
+
| `actionTypes.<A>.parameters` | foundation | action parameter: "Parameters are the inputs of an action type." | name → `{cite, type?, optional?, values?, projectedFrom?}`; read by a condition as `{param}` or, in an expression, `{ref}`; `type` is needed when an expression reads it; `optional: true` lets the caller omit it; `values` (only with `type: enum`) is the value domain of the parameter; `projectedFrom` lists the request fields, each a declared parameter of the same action, that the project resolves to this ref; it takes effect only on a `type: {ref: <Object>}` parameter with at least one field, and `decide` requires the binding of such a projected parameter, also when optional: an absent binding is `INVALID_BINDING` `missing` (section 9); on any other parameter R1 reports it and `decide` ignores it; R1, R2 |
|
|
61
|
+
| `conditions` | foundation + constraint | submission criteria: "the conditions that determine whether an action can be submitted"; Palantir submission criteria ≈ the rows whose disposition class is `rejected` | named Boolean claims `{id, expr \| unspecified, reads?, reads_unspecified?, cites[{cite}], scenarios?}`: `expr` is the claim as an expression (section 9); `unspecified` says it is not written as one, and `reads` may then list what it reads (properties, parameters `{param, values?}`, the caller `{actor: id}` / `{actor: keys}` / `{actor: kind}`); R1, R4, R12 |
|
|
62
62
|
| `decision` | extension | none | decision table `{hitPolicy: first, order?: {cite}, rows: [{when: {passes \| fails: <condition>}, result, cites, next?}], otherwise?: {result, cites, next?}}`; `next` is `{actions}`, `{external, cite}` or `{unspecified}`; R1, R12 |
|
|
63
63
|
| `edits` / `creates` / `deletes` | foundation | action rules: modify / create / delete object | `deletes` is a hard delete of datasource rows; a soft delete is an `edits` |
|
|
64
64
|
| `link_effects` | foundation + constraint | action rules: create / delete link | every incident link needs an entry, including `none` / `unspecified` (R6 sweep) |
|
|
@@ -67,7 +67,7 @@ The definitions quoted in the "Palantir analog" column are short quotations from
|
|
|
67
67
|
| `permission` … `principals` | extension | none | optional on a keys-form, an `any_of` alternative or the `{none}` form: `{kinds: [<actor kind>, …], cite}`; an actor of another kind is denied; the cite is checked by R2, not by R10; R1, R2 |
|
|
68
68
|
| `scope` | extension | none | optional tenant scope `{by, global?: [{type, cite}], root?: {type, cite}, cite}`; unlisted object types are tenant types; `cite` supports `by` only; R1, R2, R6 |
|
|
69
69
|
| `dispositions` | extension | none | optional result vocabulary `{<ID>: {class: rejected \| recorded \| pending \| applied, cite}}`; decision rows name these ids; R1, R2 |
|
|
70
|
-
| `stateMachines` | extension | none | R3 |
|
|
70
|
+
| `stateMachines` | extension | none | R1, R3; `stateMachines.<O>.property` (optional) names the property of `<O>` that holds its state; absent, it is `config.stateProperty` |
|
|
71
71
|
| `idempotencyKey` | extension | none | `{required, cite}`: whether the caller must supply an idempotency key. Absent = unspecified (not `false`). A missing key on a `required: true` action is rejected at request shape, before `decide` (so before its shape step), permission and the envelope; it is not an invocation result of `decide`, gives no receipt and is not reconciled. Never derived from effects. R1, R2 |
|
|
72
72
|
| `evidence` | extension | none | id of the executed evidence, resolved by the evidence adapter; R1, R6, R7 |
|
|
73
73
|
| `cite` / `knownSourceGaps` | extension | none | provenance and waivers; R2 |
|
|
@@ -115,10 +115,10 @@ The adapter may read the ontology instance (for example to list action ids in a
|
|
|
115
115
|
|
|
116
116
|
| Rule | Checks | Facts used | Guarantee | Not guaranteed |
|
|
117
117
|
|---|---|---|---|---|
|
|
118
|
-
| R1 | shape, references resolve (a `{param}` read names a parameter of its action; `actor` is `id` or `
|
|
118
|
+
| R1 | shape, references resolve (a `{param}` read names a parameter of its action; `actor` is `id`, `keys` or `kind`), ids unique, `formatVersion`, evidence ids exist; expressions type-check (section 9; the properties, parameters and derived properties they read declare `type`; `self` only in derived expressions; quantifier variables do not reuse reserved or bound names; set literals are non-empty with one element type), derived properties have exactly one of `expr` / `reads` and form no cycle, a derived `expr` has `type` and `cite`; a condition has exactly one of `expr` / `unspecified`, `reads` only with `unspecified`; duplicate condition ids are R1 (implementation decision: each entry with an `expr` is type-checked, but every entry with that id takes its type error, R4 reads and literals, and the R12 derived-property part from the last entry with that id that has an `expr`; the static UNKNOWN analysis also uses that last entry, while `decide` evaluates the first entry with that id); `scope` types are declared object types, each listed once; disposition ids are not invocation results; decision rows name a condition of the action and a disposition, only `pending` results have `next` (and must), `next.actions` are declared actions; `principals` is `{kinds: [distinct], cite}`; `idempotencyKey` is `{required: boolean, cite}`; a parameter with `values` has `type: enum` and a non-empty list of distinct non-empty strings, and a string literal compared with that parameter (`eq` / `neq` with `{ref: <name>}` on the other side, or every element of the set literal in `in [{ref: <name>}, {lit: [..]}]`) must be one of its `values` (else a type error); a parameter with `projectedFrom` has `type: {ref: <Object>}` and a non-empty list of distinct non-empty field names, and then each field names a declared parameter of the same action that is not projected (a request input); `stateMachines.<O>.property`, when present, is a non-empty string that names a declared property of `<O>` (key `stateMachines.<O>.property`); `canonical_values` on the state property of an object with a machine (`stateMachines.<O>.property`, default `config.stateProperty`) are states of that machine; with `config.idempotencyDeclarationRequired: true` (default `false`) an action without `idempotencyKey` fails `idempotency_undeclared` (key `<ID>`) | `config.actionIdPattern`, `config.stateProperty`, `evidence` keys, `config.idempotencyDeclarationRequired` | structure is well-formed | that an expression means what its cite says |
|
|
119
119
|
| R2 | anchors unique, quotes verbatim (also under `derivedProperties`, `scope` and `dispositions`), properties are fields, names in the store catalog, parameter names appear in their quote (`parameter_not_in_quote`), or, for a projected parameter, each of its fields (`projection_field_not_in_quote`, key `<ID>:<name>:<field>`), and so does each of a parameter's `values` (`parameter_value_not_in_quote`, key `<ID>:<name>:<value>`); with `scope`: each tenant type's field list has `<by>` (`scope_field_missing`, key `<Object>.<by>`), no global type's field list has it (`scope_field_present`), `<by>` is in the scope quote (`scope_by_not_in_quote`, key `scope`); each disposition id is in its own quote (`disposition_not_in_quote`, key `dispositions.<ID>`); a declared `type` has a cite (`type_without_provenance`, key `<Object>.<prop>` or `derivedProperties.<id>`; a derived property with `expr` and no cite is R1 only) | `section`, `containsTerm`, `fieldList`, `storeCatalog` | quotes exist in the source | that a quote supports the claim |
|
|
120
|
-
| R3 | graph equality, terminal states, reachability, `by` = triggers, `by` iff the action edits the state property | `stateMachines`, `transitionTriggers`, `config.stateProperty` | ontology matches the source machine | trigger-cell conditions; objects without a machine in the source |
|
|
121
|
-
| R4 | conditions read only canonical and policy properties, for an expression also through the derived properties it reaches (parameter and actor reads are request inputs, not state, and are not classified; a derived property without `expr` is not expanded; a hand-written `reads` entry that names a derived property is expanded through its `reads`, or, when it has an `expr`, through the properties that expression reads, as for an expression condition); a non-canonical property passes when every read compares it by `eq` / `neq` with a literal in its `canonical_values`; canonical values and the values of a parameter read appear in their quote (`parameter_value_unsupported`); each literal of an expression appears in one of its condition's quotes, or in the derived property's quote (`literal_unsupported`, key `<ID>:<condition id>:<value>` or `derivedProperties.<id>:<value>`) | none | the property reads of conditions are canonical or policy facts; literals are not invented | that `reads` is complete; that an expression's logic matches its cite |
|
|
120
|
+
| R3 | graph equality, terminal states, reachability, `by` = triggers, `by` iff the action edits the state property of the object (`stateMachines.<O>.property`, default `config.stateProperty`) | `stateMachines`, `transitionTriggers`, `config.stateProperty` | ontology matches the source machine | trigger-cell conditions; objects without a machine in the source |
|
|
121
|
+
| R4 | conditions read only canonical and policy properties, for an expression also through the derived properties it reaches (parameter and actor reads are request inputs, not state, and are not classified; a derived property without `expr` is not expanded; a hand-written `reads` entry that names a derived property is expanded through its `reads`, or, when it has an `expr`, through the properties that expression reads, as for an expression condition); a non-canonical property passes when every read compares it by `eq` / `neq` with a literal in its `canonical_values`; canonical values and the values of a parameter read appear in their quote (`parameter_value_unsupported`); each literal of an expression appears in one of its condition's quotes (an empty string never appears), or in the derived property's quote (`literal_unsupported`, key `<ID>:<condition id>:<value>` or `derivedProperties.<id>:<value>`) | none | the property reads of conditions are canonical or policy facts; literals are not invented | that `reads` is complete; that an expression's logic matches its cite |
|
|
122
122
|
| R5 | object membership, cross-context declaration (`via` or `unspecified`), `create_rule`, entity coverage | `contextMembers`, `config.crossContextMechanisms` | object ownership matches the source | the content of the mechanism |
|
|
123
123
|
| R6 | link sweep, link catalog complete (with `scope`, the `<by>` field of a tenant type counts as declared and must not be in `non_link_fields`: `non_link_field_stray`), `none_but_written` | `fieldList`, `config.linkFieldPattern`, `evidence` | link effects are declared completely | that an effect text is right |
|
|
124
124
|
| R7 | observed writes are declared; events are in `emits`; changed columns are declared (`column_undeclared`, key `<ID>:<Object>.<prop>`); declared effects are witnessed (`effect_unwitnessed`); with `config.evidenceRequired: true` (default `false`) an action without `evidence` fails `evidence_missing` (key `<ID>`) | `evidence`, `config.evidenceRequired` | every store changed by a committed invocation is in the touched objects' `datasource`, a pure link table with an `effect`, or `infrastructureStores`; every observed event is in `emits`; every column changed on an existing row of a touched object's `datasource` is, for each touched object of that store, a property of a resolvable `edits` entry of that same object (objects may share a store; a column that one object declares is still reported for each other touched object of the store that does not declare it); every resolvable `edits` entry (resolved as R1 resolves it; an unresolvable entry is R1 only) was changed on an existing row (key `<ID>:<Object>.<prop>`), every `creates` object had a row inserted (key `<ID>:creates:<Object>`), every `deletes` object had a row deleted (key `<ID>:deletes:<Object>`) and every pure link table with an `effect` had a row inserted or deleted (key `<ID>:link:<link id>`) by at least one committed invocation | actions without evidence (unless `evidenceRequired`); that a not yet witnessed effect is unreachable; atomicity, which refers to the committed effect (one invocation may have several attempts) and which the project profile or evidence adapter must enforce |
|
|
@@ -157,7 +157,7 @@ The table lists only the optional facts. Every rule may also read the required f
|
|
|
157
157
|
|
|
158
158
|
## 8. Versions
|
|
159
159
|
|
|
160
|
-
The version is in two places: `v1` in the schema `$id`, and `formatVersion: 1` in the ontology file (R1 fails on any other value). A change that breaks existing ontologies raises both. A change that rejects only files without meaning (for example an empty name) is not a break. Adding the `permission.any_of` form is additive: existing files stay valid, so `formatVersion` stays 1. Adding `parameters` and the parameter and actor reads of the submission criteria (since renamed `conditions`) is additive: existing files stay valid, so `formatVersion` stays 1. Adding `values` on enum parameters is additive; `formatVersion` stays 1. Adding `deletes` is additive; `formatVersion` stays 1. Adding `projectedFrom` on ref parameters is additive; `formatVersion` stays 1.
|
|
160
|
+
The version is in two places: `v1` in the schema `$id`, and `formatVersion: 1` in the ontology file (R1 fails on any other value). A change that breaks existing ontologies raises both. A change that rejects only files without meaning (for example an empty name) is not a break. Adding the `permission.any_of` form is additive: existing files stay valid, so `formatVersion` stays 1. Adding `parameters` and the parameter and actor reads of the submission criteria (since renamed `conditions`) is additive: existing files stay valid, so `formatVersion` stays 1. Adding `values` on enum parameters is additive; `formatVersion` stays 1. Adding `deletes` is additive; `formatVersion` stays 1. Adding `projectedFrom` on ref parameters is additive; `formatVersion` stays 1. Adding the ref `actor.kind` (and the read `{actor: kind}`) is additive; `formatVersion` stays 1. Requiring each `projectedFrom` field to be a declared parameter rejects only files that fail at run time (the field reaches `decide` as an undeclared binding, `INVALID_BINDING` `unknown_param`); `formatVersion` stays 1. For an optional projected parameter whose field is missing or null, the projection now passes null (it omitted the binding before); `decide` is unchanged, and `formatVersion` stays 1.
|
|
161
161
|
During 0.x the v1 draft changes in place (`formatVersion` 1, `$id` v1); the rule above applies from the 1.0 semantic freeze.
|
|
162
162
|
|
|
163
163
|
## 9. Decision semantics
|
|
@@ -176,7 +176,7 @@ Every node is a single-key map. The reserved names `now`, `actor` and `self` are
|
|
|
176
176
|
|
|
177
177
|
`{passes: c}` and `{fails: c}` are not expression nodes; they occur only in the `when` of a decision row. An omitted `where` is T.
|
|
178
178
|
|
|
179
|
-
Names in `{ref}` (at most one dot): `now` (timestamp), `actor.id` (id), `actor.keys` (`{set: string}`), a parameter, a quantifier variable, and `self` (only in a derived property expression). `x.prop` reads a declared, typed property of the object binding `x`; a derived property is read only through `{derived}`; `x.id` is not supported (write `{ref: x}`). A quantifier variable must not reuse a reserved name, a parameter name or an outer variable. A derived expression sees only `self`, `now`, `actor` and its own quantifier variables.
|
|
179
|
+
Names in `{ref}` (at most one dot): `now` (timestamp), `actor.id` (id), `actor.keys` (`{set: string}`), `actor.kind` (`enum`: the kind of the caller, as in `principals`; no value domain is declared, so the value of a literal compared with it is checked only by R4 `literal_unsupported`, and, as for any two `enum` values, a comparison with another `enum` property or parameter type-checks whether or not it has a meaning), a parameter, a quantifier variable, and `self` (only in a derived property expression). `x.prop` reads a declared, typed property of the object binding `x`; a derived property is read only through `{derived}`; `x.id` is not supported (write `{ref: x}`). A quantifier variable must not reuse a reserved name, a parameter name or an outer variable. A derived expression sees only `self`, `now`, `actor` and its own quantifier variables.
|
|
180
180
|
|
|
181
181
|
### Types and values
|
|
182
182
|
|
|
@@ -230,7 +230,7 @@ Soundness contract: for each combination of null / non-null over the nullable re
|
|
|
230
230
|
Nullability comes from the facts and the parameters; only this analysis treats unknown nullability as null:
|
|
231
231
|
|
|
232
232
|
- `fieldList` gives each field with `nullable` (a trailing `?` in the source). A field is non-null only when its object's field list says `nullable: false`. Unknown nullability is analysed as may-be-null: a field that is not listed, or an object without a field list (R2 reports both).
|
|
233
|
-
- `now` and `actor.id` / `actor.keys` are never null. A quantifier variable or `self` is not null; its property `x.prop` may be null unless the field is non-null.
|
|
233
|
+
- `now` and `actor.id` / `actor.keys` / `actor.kind` are never null. A quantifier variable or `self` is not null; its property `x.prop` may be null unless the field is non-null.
|
|
234
234
|
- An optional parameter may be null, and so may every property read through it. A required parameter is not null (a missing one, or a null projected one, is `INVALID_BINDING`); its property may be null unless the field is non-null.
|
|
235
235
|
|
|
236
236
|
Counting: the analysis collects the distinct normalized paths that may be null and that the outer evaluation reads (`self` is replaced by the `of` of the derived property that is inlined). Reads only inside a quantifier are not counted, because a quantifier is T or F outside; a read that also occurs outside a quantifier is counted there. The `from` of a navigation is never counted: a null ref navigates to the empty set, not to U. With k paths, all 2^k combinations are evaluated; for k > 8 (`MAX_NULLABLE_REFS`) the condition is taken as may-be-U without enumeration. A quantifier body is analysed on its own with the same evaluator and the same counting, its variable added to the visible names; a quantifier nested in the body is again T or F there and is analysed on its own.
|
|
@@ -283,15 +283,15 @@ A request-shape check before `decide` (so before its shape step), permission and
|
|
|
283
283
|
|
|
284
284
|
The invocation results are a fixed set (`INVOCATION_RESULTS`), not declared by actions. A null optional ref binds; reading its properties gives null. Invalid input (unknown action, scope argument against the declaration, an ill-typed condition, an untyped parameter, a parameter named `now`, `actor` or `self`, a decision row naming an unknown condition, a decision row or `otherwise` whose `result` has no disposition, a type both global and root, a corrupt or incomplete snapshot) throws; it is never a result. A row without a field that evaluation reads (such as the scope field) is incomplete; a field with the value null is not.
|
|
285
285
|
|
|
286
|
-
Projected parameters. A `{ref: T}` parameter with `projectedFrom: [<field>, …]` (at least one field) is projected: it is not a field of the request. The project's projection reads those request fields (an alternate key, for example `source` and `external_ref`) and looks up the object of the parameter's ref type as the runtime looks it up: with the same scope rules (only in the caller's scope for a tenant or root type; everywhere for a global type) and the same soft delete filter. It never passes the id of an object outside the scope. When
|
|
286
|
+
Projected parameters. A `{ref: T}` parameter with `projectedFrom: [<field>, …]` (at least one field) is projected: it is not a field of the request. Each of its fields is a declared parameter of the same action that is not itself projected (R1): a request input, given and checked as any other parameter. The projection only adds the projected binding; it does not remove the fields from the bindings. The project's projection reads those request fields (an alternate key, for example `source` and `external_ref`) and looks up the object of the parameter's ref type as the runtime looks it up: with the same scope rules (only in the caller's scope for a tenant or root type; everywhere for a global type) and the same soft delete filter. It never passes the id of an object outside the scope. When any of the fields is missing from the request or is null, the projection does not look up: for an optional parameter it passes null, and for a required parameter it omits the binding. Else, when no object has the key, it passes null; else it passes the id. Null is not the same as an absent binding:
|
|
287
287
|
|
|
288
288
|
| Binding | Required parameter | Optional parameter |
|
|
289
289
|
|---|---|---|
|
|
290
|
-
| absent | `INVALID_BINDING` `missing` (shape) | `INVALID_BINDING` `missing` (shape) |
|
|
291
|
-
| null | `INVALID_BINDING` `not_found` (binding and scope), as for an id that does not resolve | an ordinary value, for example "no other object has this key", which a condition tests with `isNull` |
|
|
290
|
+
| absent | `INVALID_BINDING` `missing` (shape): a field is missing from the request or is null | `INVALID_BINDING` `missing` (shape): the projection did not give the binding, an error of the project |
|
|
291
|
+
| null | `INVALID_BINDING` `not_found` (binding and scope), as for an id that does not resolve | an ordinary value: a field is missing or null, or no object has the key; for example "no other object has this key", which a condition tests with `isNull` |
|
|
292
292
|
| id | bound as any ref parameter | bound as any ref parameter |
|
|
293
293
|
|
|
294
|
-
|
|
294
|
+
For a required parameter, an absent binding is the expected input when a field is missing (`decide` gives `missing`); when the runtime gives another result, `reconcile` shows the difference as a mismatch. For an optional parameter, an absent binding is never a valid input: `decide` gives `missing`, so a projection that did not run shows as a mismatch in `reconcile` and never passes as "no object has this key". When a field is a required parameter and is missing or null, the result is `INVALID_BINDING` `missing` whatever the projection passes; the rule for an optional parameter matters only when its fields are optional. On any other parameter (another type, for example `{set: {ref: T}}`, or no field) R1 reports `projectedFrom` and `decide` ignores it: the parameter behaves as without `projectedFrom`. The format does not read the fields or their values; it has no alternate-key model.
|
|
295
295
|
|
|
296
296
|
### Decision table and INDETERMINATE
|
|
297
297
|
|
package/dist/src/checker.js
CHANGED
|
@@ -100,6 +100,8 @@ export function check(ontology, facts) {
|
|
|
100
100
|
for (const n of Object.keys(stateMachines))
|
|
101
101
|
if (!objType(n))
|
|
102
102
|
v("R1", `stateMachines.${n}`, "not a declared object type");
|
|
103
|
+
// the state property of an object: its machine's `property` when that is a non-empty string, else the profile default
|
|
104
|
+
const stateProp = (o) => (own(stateMachines, o) && isObj(stateMachines[o]) && isStr(stateMachines[o].property) ? stateMachines[o].property : config.stateProperty);
|
|
103
105
|
const gaps = arr(ont.knownSourceGaps);
|
|
104
106
|
if (!Array.isArray(ont.knownSourceGaps))
|
|
105
107
|
v("R1", "knownSourceGaps", "missing (use [])");
|
|
@@ -135,7 +137,7 @@ export function check(ontology, facts) {
|
|
|
135
137
|
v("R1", `${p}.properties.${pn}.canonical_values`, "needs values[] and cite");
|
|
136
138
|
else if (pr.class === "canonical")
|
|
137
139
|
v("R1", `${p}.properties.${pn}.canonical_values`, "only meaningful on a non-canonical property");
|
|
138
|
-
else if (pn ===
|
|
140
|
+
else if (pn === stateProp(name) && states)
|
|
139
141
|
for (const x of cv.values)
|
|
140
142
|
if (!states.has(x))
|
|
141
143
|
v("R1", `${p}.properties.${pn}.canonical_values`, `${x} is not a ${name} state`);
|
|
@@ -152,6 +154,14 @@ export function check(ontology, facts) {
|
|
|
152
154
|
const cls = objects[o].properties[p].class;
|
|
153
155
|
return CLASSES.includes(cls) ? cls : null; // a property without a valid class does not resolve (R1 reports it)
|
|
154
156
|
};
|
|
157
|
+
for (const [n, sm] of Object.entries(stateMachines)) {
|
|
158
|
+
if (!objType(n) || !isObj(sm) || sm.property === undefined)
|
|
159
|
+
continue; // an undeclared object or a machine that is not a mapping is R1 above / below
|
|
160
|
+
if (!isStr(sm.property))
|
|
161
|
+
v("R1", `stateMachines.${n}.property`, "must be a non-empty string");
|
|
162
|
+
else if (propClass(`${n}.${sm.property}`) === null)
|
|
163
|
+
v("R1", `stateMachines.${n}.property`, `${sm.property} does not resolve to a declared property of ${n}`);
|
|
164
|
+
}
|
|
155
165
|
const linkIds = new Set();
|
|
156
166
|
for (const [i, l] of links.entries()) {
|
|
157
167
|
const p = `linkTypes[${i}]`;
|
|
@@ -287,6 +297,11 @@ export function check(ontology, facts) {
|
|
|
287
297
|
const pf = isObj(pv) ? pv.projectedFrom : undefined;
|
|
288
298
|
if (pf !== undefined && !(isObj(pv.type) && isStr(pv.type.ref) && Array.isArray(pf) && pf.length > 0 && pf.every(isStr) && new Set(pf).size === pf.length))
|
|
289
299
|
v("R1", `${p}.parameters.${n}`, "projectedFrom needs type {ref: <Object>} and a non-empty list of distinct non-empty field names");
|
|
300
|
+
// Each field is a request input: the projection reads it from the request and only adds its own binding.
|
|
301
|
+
else if (pf !== undefined)
|
|
302
|
+
for (const f of pf)
|
|
303
|
+
if (!own(params, f) || projectedFields(isObj(params[f]) ? params[f] : {}) !== null)
|
|
304
|
+
v("R1", `${p}.parameters.${n}`, `projectedFrom field ${f} must be a declared parameter of ${cid} that is not projected`);
|
|
290
305
|
}
|
|
291
306
|
const pids = new Set();
|
|
292
307
|
for (const [i, pc] of arr(c.conditions).entries()) {
|
|
@@ -309,8 +324,8 @@ export function check(ontology, facts) {
|
|
|
309
324
|
v("R1", `${p}.conditions.${pc.id}`, `parameter read ${JSON.stringify(r)}: values must be a non-empty list of non-empty strings`);
|
|
310
325
|
}
|
|
311
326
|
else if (isObj(r) && "actor" in r) {
|
|
312
|
-
if (r.actor !== "id" && r.actor !== "keys")
|
|
313
|
-
v("R1", `${p}.conditions.${pc.id}`, `actor read ${JSON.stringify(r)} must be {actor: id} or {actor:
|
|
327
|
+
if (r.actor !== "id" && r.actor !== "keys" && r.actor !== "kind")
|
|
328
|
+
v("R1", `${p}.conditions.${pc.id}`, `actor read ${JSON.stringify(r)} must be {actor: id}, {actor: keys} or {actor: kind}`);
|
|
314
329
|
}
|
|
315
330
|
else if (isObj(r)) {
|
|
316
331
|
if (!isStr(r.prop) || propClass(r.prop) === null || !Array.isArray(r.values) || r.values.length === 0)
|
|
@@ -549,13 +564,13 @@ export function check(ontology, facts) {
|
|
|
549
564
|
for (const [cid, c] of Object.entries(commands)) {
|
|
550
565
|
if (!isObj(c))
|
|
551
566
|
continue;
|
|
552
|
-
const stateEdits = new Set(arr(c.edits).filter((e) => e.endsWith(`.${
|
|
567
|
+
const stateEdits = new Set(arr(c.edits).filter((e) => e.endsWith(`.${stateProp(e.split(".")[0])}`)).map((e) => e.split(".")[0]));
|
|
553
568
|
for (const o of stateEdits)
|
|
554
569
|
if (objType(o) && isObj(stateMachines[o]) && !byCmds.get(o)?.has(cid))
|
|
555
|
-
v("R3", `${cid}:${o}.${
|
|
570
|
+
v("R3", `${cid}:${o}.${stateProp(o)}`, `${cid} edits ${o}.${stateProp(o)} but is in no ${o} transition's by`);
|
|
556
571
|
for (const [o, cs] of byCmds)
|
|
557
572
|
if (cs.has(cid) && !stateEdits.has(o))
|
|
558
|
-
v("R3", `${cid}:${o}.${
|
|
573
|
+
v("R3", `${cid}:${o}.${stateProp(o)}`, `${cid} is in a ${o} transition's by but does not edit ${o}.${stateProp(o)}`);
|
|
559
574
|
}
|
|
560
575
|
// ------------------------------------------------------------------------------------------ R4
|
|
561
576
|
for (const [cid, c] of Object.entries(commands)) {
|
package/dist/src/contract.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type RuleId = "R1" | "R2" | "R3" | "R4" | "R5" | "R6" | "R7" | "R8" | "R9" | "R10" | "R11" | "R12";
|
|
2
2
|
export declare const RULES: readonly RuleId[];
|
|
3
|
-
/** A name as a whole identifier: not preceded or followed by a letter, a digit or `_`. */
|
|
3
|
+
/** A name as a whole identifier: not preceded or followed by a letter, a digit or `_`. The empty string matches nothing. */
|
|
4
4
|
export declare const IDENT: (s: string) => RegExp;
|
|
5
5
|
export interface Source {
|
|
6
6
|
anchor: string;
|
package/dist/src/contract.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export const RULES = ["R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R11", "R12"];
|
|
2
|
-
/** A name as a whole identifier: not preceded or followed by a letter, a digit or `_`. */
|
|
3
|
-
export const IDENT = (s) => new RegExp(`(?<![A-Za-z0-9_])${s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_])`);
|
|
2
|
+
/** A name as a whole identifier: not preceded or followed by a letter, a digit or `_`. The empty string matches nothing. */
|
|
3
|
+
export const IDENT = (s) => (s === "" ? /(?!)/ : new RegExp(`(?<![A-Za-z0-9_])${s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_])`));
|
package/dist/src/evaluate.js
CHANGED
|
@@ -127,7 +127,9 @@ function readRef(s, f, path) {
|
|
|
127
127
|
return s.env.actor.id;
|
|
128
128
|
if (prop === "keys")
|
|
129
129
|
return s.env.actor.keys;
|
|
130
|
-
|
|
130
|
+
if (prop === "kind")
|
|
131
|
+
return s.env.actor.kind;
|
|
132
|
+
throw new Error(`actor.${prop ?? ""} is not readable; use actor.id, actor.keys or actor.kind`);
|
|
131
133
|
}
|
|
132
134
|
const o = objVar(s, f, head);
|
|
133
135
|
if (prop === undefined) {
|
package/dist/src/expr.js
CHANGED
|
@@ -147,7 +147,9 @@ function refType(ont, path, ctx, at) {
|
|
|
147
147
|
return "id";
|
|
148
148
|
if (prop === "keys")
|
|
149
149
|
return { set: "string" };
|
|
150
|
-
|
|
150
|
+
if (prop === "kind")
|
|
151
|
+
return "enum";
|
|
152
|
+
throw new ExprError(at, `actor.${prop ?? ""} is not readable; use actor.id, actor.keys or actor.kind`);
|
|
151
153
|
}
|
|
152
154
|
if (head === "self" && ctx.self === undefined)
|
|
153
155
|
throw new ExprError(at, "self is valid only in a derived property expression");
|
package/dist/src/query.d.ts
CHANGED
package/dist/src/query.js
CHANGED
package/docs/adapter-guide.md
CHANGED
|
@@ -61,7 +61,7 @@ Parameters and parameter and actor reads (`parameters`, `{param}`, `{actor}`) ne
|
|
|
61
61
|
- `crossContextMechanisms`: the legal `via` values of `crossContext` (R5).
|
|
62
62
|
- `actionIdPattern`: the pattern of an action id (R1).
|
|
63
63
|
- `linkFieldPattern`: the pattern of a field that holds a link (R6).
|
|
64
|
-
- `stateProperty`: the name of the state property (R1, R3).
|
|
64
|
+
- `stateProperty`: the default name of the state property (R1, R3). An ontology machine names another property of its object with `stateMachines.<Object>.property`.
|
|
65
65
|
|
|
66
66
|
## 6. Run the example
|
|
67
67
|
|
package/ontology.schema.json
CHANGED
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
"values",
|
|
316
316
|
"cite"
|
|
317
317
|
],
|
|
318
|
-
"description": "Values of a NON-canonical property that the source explicitly marks as canonical. Extension (R1, R4); no Palantir analog. R1: needs values[] (non-empty) and cite; not allowed when class is canonical; on the
|
|
318
|
+
"description": "Values of a NON-canonical property that the source explicitly marks as canonical. Extension (R1, R4); no Palantir analog. R1: needs values[] (non-empty) and cite; not allowed when class is canonical; on the state property of an object with `stateMachines` (`stateMachines.<Object>.property`, default the configured state property), each value must be a state of that object. R4: each value must occur as a whole identifier in this cite's quote (else R4 `canonical_value_unsupported`); a condition value read `{prop, values}` of this property passes R4 only if all its values are in this list.",
|
|
319
319
|
"properties": {
|
|
320
320
|
"values": {
|
|
321
321
|
"type": "array",
|
|
@@ -409,12 +409,17 @@
|
|
|
409
409
|
"terminal",
|
|
410
410
|
"transitions"
|
|
411
411
|
],
|
|
412
|
-
"description": "State machine of the object. Extension (R1, R3); no Palantir analog. R3 compares it with the machine the adapter extracts from the source: the state set, the initial state and the transition set must be equal; `terminal` must equal the source states that have no outgoing edge; every source state must be reachable from the initial state. Also R3: an action appears in some `by` of this object's transitions if and only if its `edits` contains `<Object>.<state property>`.",
|
|
412
|
+
"description": "State machine of the object. Extension (R1, R3); no Palantir analog. R3 compares it with the machine the adapter extracts from the source: the state set, the initial state and the transition set must be equal; `terminal` must equal the source states that have no outgoing edge; every source state must be reachable from the initial state. Also R3: an action appears in some `by` of this object's transitions if and only if its `edits` contains `<Object>.<state property>`. The state property is `property` when it is given, else the configured state property (profile `stateProperty`, default `state`).",
|
|
413
413
|
"properties": {
|
|
414
414
|
"doc": {
|
|
415
415
|
"$ref": "#/$defs/anchor",
|
|
416
416
|
"description": "The anchor of the source section that holds the state machine (R3: must be exactly the anchor the adapter reports for this object)."
|
|
417
417
|
},
|
|
418
|
+
"property": {
|
|
419
|
+
"type": "string",
|
|
420
|
+
"minLength": 1,
|
|
421
|
+
"description": "Optional. The property of the object that holds its state. Absent: the configured state property (profile `stateProperty`, default `state`). Extension (R1, R3); no Palantir analog. R1: must be a non-empty string and a declared property of the object (else R1, key `stateMachines.<Name>.property`). R1 and R3 use it in place of the configured state property for this object only: the `canonical_values` state check, and `by` if and only if `edits` contains `<Object>.<property>`."
|
|
422
|
+
},
|
|
418
423
|
"initial": {
|
|
419
424
|
"type": "string",
|
|
420
425
|
"minLength": 1,
|
|
@@ -927,7 +932,7 @@
|
|
|
927
932
|
},
|
|
928
933
|
"expr": {
|
|
929
934
|
"$ref": "#/$defs/expr",
|
|
930
|
-
"description": "The claim as a predicate expression. R1: it type-checks in the action (parameters, `actor.id`, `actor.keys`, `now`, quantifier variables). R4: every literal must occur as a whole identifier in one of the `cites` quotes (else `literal_unsupported`, key `<ID>:<condition id>:<value>`)."
|
|
935
|
+
"description": "The claim as a predicate expression. R1: it type-checks in the action (parameters, `actor.id`, `actor.keys`, `actor.kind`, `now`, quantifier variables). R4: every literal must occur as a whole identifier in one of the `cites` quotes (else `literal_unsupported`, key `<ID>:<condition id>:<value>`)."
|
|
931
936
|
},
|
|
932
937
|
"unspecified": {
|
|
933
938
|
"type": "string",
|
|
@@ -936,7 +941,7 @@
|
|
|
936
941
|
},
|
|
937
942
|
"reads": {
|
|
938
943
|
"type": "array",
|
|
939
|
-
"description": "What the criterion reads. Items: `Object.prop` (a declared property), a derived id (a key of `derivedProperties`; its `reads` are checked instead), a value read `{prop, values}`, a parameter read `{param, values?}` (a key of this action's `parameters`) or an actor read `{actor: id | keys}`. R1: each item must resolve. R4: every property read, directly or through a derived id, must be class canonical (derived, policy and unknown fail R4 `precondition_reads_non_canonical`, key `<ID>:<Object.prop>`). A value read passes if the property is canonical, or if every value is in that property's `canonical_values.values`. Parameter and actor reads are not properties; R4 does not classify them. Each value of a parameter read must appear in one of this condition's cite quotes (R4 `parameter_value_unsupported`, key `<ID>:<criterion id>:<value>`).",
|
|
944
|
+
"description": "What the criterion reads. Items: `Object.prop` (a declared property), a derived id (a key of `derivedProperties`; its `reads` are checked instead), a value read `{prop, values}`, a parameter read `{param, values?}` (a key of this action's `parameters`) or an actor read `{actor: id | keys | kind}`. R1: each item must resolve. R4: every property read, directly or through a derived id, must be class canonical (derived, policy and unknown fail R4 `precondition_reads_non_canonical`, key `<ID>:<Object.prop>`). A value read passes if the property is canonical, or if every value is in that property's `canonical_values.values`. Parameter and actor reads are not properties; R4 does not classify them. Each value of a parameter read must appear in one of this condition's cite quotes (R4 `parameter_value_unsupported`, key `<ID>:<criterion id>:<value>`).",
|
|
940
945
|
"items": {
|
|
941
946
|
"oneOf": [
|
|
942
947
|
{
|
|
@@ -979,8 +984,8 @@
|
|
|
979
984
|
"type": "object",
|
|
980
985
|
"additionalProperties": false,
|
|
981
986
|
"required": ["actor"],
|
|
982
|
-
"description": "Actor read: the precondition reads the caller — `id` (its identity)
|
|
983
|
-
"properties": { "actor": { "enum": ["id", "keys"] } }
|
|
987
|
+
"description": "Actor read: the precondition reads the caller — `id` (its identity), `keys` (its effective permission keys, for example which `permission.any_of` alternative it satisfies) or `kind` (its actor kind, as in `principals`).",
|
|
988
|
+
"properties": { "actor": { "enum": ["id", "keys", "kind"] } }
|
|
984
989
|
}
|
|
985
990
|
]
|
|
986
991
|
}
|
|
@@ -1009,7 +1014,7 @@
|
|
|
1009
1014
|
"type": "object",
|
|
1010
1015
|
"additionalProperties": false,
|
|
1011
1016
|
"required": ["cite"],
|
|
1012
|
-
"description": "One action parameter. Foundation: Palantir action parameter. The name is the map key; the cite is the source text that names it. `type` is required when an expression reads the parameter; `optional: true` lets the caller omit it (it is then null). `values` is the value domain of an `enum` parameter (only with `type: enum`): a value outside it is `INVALID_BINDING` `type`; R1 checks its shape and that literals compared with the parameter are among them, R2 that each value appears in the cite quote. `projectedFrom` (only with `type: {ref: <Object>}`) lists the request fields, an alternate key, that the project's projection resolves to this ref; on any other parameter R1 reports it and `decide` ignores it; the cite quote must name each field instead of the parameter name (R2 `projection_field_not_in_quote`); a null value of a required projected parameter is `INVALID_BINDING` `not_found`, and an absent one is `missing`, also with `optional: true` (SPEC section 9).",
|
|
1017
|
+
"description": "One action parameter. Foundation: Palantir action parameter. The name is the map key; the cite is the source text that names it. `type` is required when an expression reads the parameter; `optional: true` lets the caller omit it (it is then null). `values` is the value domain of an `enum` parameter (only with `type: enum`): a value outside it is `INVALID_BINDING` `type`; R1 checks its shape and that literals compared with the parameter are among them, R2 that each value appears in the cite quote. `projectedFrom` (only with `type: {ref: <Object>}`) lists the request fields, an alternate key, that the project's projection resolves to this ref; each field is a declared parameter of the same action that is not projected (R1); on any other parameter R1 reports it and `decide` ignores it; the cite quote must name each field instead of the parameter name (R2 `projection_field_not_in_quote`); a null value of a required projected parameter is `INVALID_BINDING` `not_found`, and an absent one is `missing`, also with `optional: true` (SPEC section 9).",
|
|
1013
1018
|
"properties": {
|
|
1014
1019
|
"cite": { "$ref": "#/$defs/cite" },
|
|
1015
1020
|
"type": { "$ref": "#/$defs/valueType" },
|
|
@@ -1135,7 +1140,7 @@
|
|
|
1135
1140
|
"ref": {
|
|
1136
1141
|
"type": "string",
|
|
1137
1142
|
"minLength": 1,
|
|
1138
|
-
"description": "now, actor.id, actor.keys, a parameter, or <binding>.<property> (one dot)."
|
|
1143
|
+
"description": "now, actor.id, actor.keys, actor.kind, a parameter, or <binding>.<property> (one dot)."
|
|
1139
1144
|
},
|
|
1140
1145
|
"lit": {
|
|
1141
1146
|
"oneOf": [
|
package/package.json
CHANGED
package/vocab.ttl
CHANGED
|
@@ -137,6 +137,11 @@ ofv:stateMachines a rdf:Property ;
|
|
|
137
137
|
rdfs:comment "State machine of an object type. YAML: stateMachines.<name>" ;
|
|
138
138
|
rdfs:isDefinedBy <https://w3id.org/avouch/v1/vocab> .
|
|
139
139
|
|
|
140
|
+
ofv:property a rdf:Property ;
|
|
141
|
+
rdfs:label "property" ;
|
|
142
|
+
rdfs:comment "Property of the object that holds its state; default the configured state property. YAML: stateMachines.<name>.property" ;
|
|
143
|
+
rdfs:isDefinedBy <https://w3id.org/avouch/v1/vocab> .
|
|
144
|
+
|
|
140
145
|
ofv:initial a rdf:Property ;
|
|
141
146
|
rdfs:label "initial" ;
|
|
142
147
|
rdfs:comment "Initial state. YAML: stateMachines.<name>.initial" ;
|
|
@@ -324,7 +329,7 @@ ofv:param a rdf:Property ;
|
|
|
324
329
|
|
|
325
330
|
ofv:actor a rdf:Property ;
|
|
326
331
|
rdfs:label "actor" ;
|
|
327
|
-
rdfs:comment "The caller read by a criterion: id or
|
|
332
|
+
rdfs:comment "The caller read by a criterion: id, keys or kind. YAML: actionTypes.<ID>.conditions[].reads[].actor" ;
|
|
328
333
|
rdfs:isDefinedBy <https://w3id.org/avouch/v1/vocab> .
|
|
329
334
|
|
|
330
335
|
ofv:submissionCriteria a rdf:Property ;
|
|
@@ -480,7 +485,7 @@ ofv:optional a rdf:Property ;
|
|
|
480
485
|
|
|
481
486
|
ofv:projectedFrom a rdf:Property ;
|
|
482
487
|
rdfs:label "projectedFrom" ;
|
|
483
|
-
rdfs:comment "Request fields (an alternate key) that the project's projection resolves to this ref parameter.
|
|
488
|
+
rdfs:comment "Request fields (an alternate key; each a declared parameter of the same action that is not projected) that the project's projection resolves to this ref parameter. An optional projected parameter is always given: null when a field is missing or null, or when no object has the key. A required one is omitted when a field is missing or null, and null when no object has the key. YAML: actionTypes.<ID>.parameters.<n>.projectedFrom[]" ;
|
|
484
489
|
rdfs:isDefinedBy <https://w3id.org/avouch/v1/vocab> .
|
|
485
490
|
|
|
486
491
|
ofv:principals a rdf:Property ;
|