@githonllc/avouch 0.1.10 → 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 CHANGED
@@ -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,9 +115,9 @@ 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`, `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); 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 |
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 |
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
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 |
@@ -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 === config.stateProperty && states)
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}]`;
@@ -554,13 +564,13 @@ export function check(ontology, facts) {
554
564
  for (const [cid, c] of Object.entries(commands)) {
555
565
  if (!isObj(c))
556
566
  continue;
557
- const stateEdits = new Set(arr(c.edits).filter((e) => e.endsWith(`.${config.stateProperty}`)).map((e) => e.split(".")[0]));
567
+ const stateEdits = new Set(arr(c.edits).filter((e) => e.endsWith(`.${stateProp(e.split(".")[0])}`)).map((e) => e.split(".")[0]));
558
568
  for (const o of stateEdits)
559
569
  if (objType(o) && isObj(stateMachines[o]) && !byCmds.get(o)?.has(cid))
560
- v("R3", `${cid}:${o}.${config.stateProperty}`, `${cid} edits ${o}.${config.stateProperty} but is in no ${o} transition's by`);
570
+ v("R3", `${cid}:${o}.${stateProp(o)}`, `${cid} edits ${o}.${stateProp(o)} but is in no ${o} transition's by`);
561
571
  for (const [o, cs] of byCmds)
562
572
  if (cs.has(cid) && !stateEdits.has(o))
563
- v("R3", `${cid}:${o}.${config.stateProperty}`, `${cid} is in a ${o} transition's by but does not edit ${o}.${config.stateProperty}`);
573
+ v("R3", `${cid}:${o}.${stateProp(o)}`, `${cid} is in a ${o} transition's by but does not edit ${o}.${stateProp(o)}`);
564
574
  }
565
575
  // ------------------------------------------------------------------------------------------ R4
566
576
  for (const [cid, c] of Object.entries(commands)) {
@@ -214,6 +214,7 @@ export type QueryObject = {
214
214
  }[];
215
215
  stateMachine: {
216
216
  doc: S;
217
+ property: S;
217
218
  initial: S;
218
219
  terminal: string[];
219
220
  transitions: {
package/dist/src/query.js CHANGED
@@ -451,6 +451,7 @@ export function queryObject(ont, id) {
451
451
  stateMachine: sm
452
452
  ? {
453
453
  doc: str(sm.doc),
454
+ property: str(sm.property),
454
455
  initial: str(sm.initial),
455
456
  terminal: strs(sm.terminal),
456
457
  transitions: arr(sm.transitions)
@@ -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
 
@@ -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 configured state property of an object with `stateMachines`, 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.",
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githonllc/avouch",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
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" ;