@githonllc/avouch 0.1.2 → 0.1.4
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 +9 -3
- package/SPEC.md +4 -4
- package/dist/src/checker.js +19 -7
- package/dist/src/query.d.ts +3 -0
- package/dist/src/query.js +7 -4
- package/ontology.schema.json +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,10 +129,12 @@ claude plugin install avouch@avouch
|
|
|
129
129
|
Inside a Claude Code session, the same two steps are `/plugin marketplace add githonllc/avouch` and `/plugin install avouch@avouch`.
|
|
130
130
|
|
|
131
131
|
The plugin has two skills:
|
|
132
|
-
- `avouch
|
|
133
|
-
- `
|
|
132
|
+
- `cli` (`/avouch:cli`): checks an ontology and runs `avouch query`.
|
|
133
|
+
- `scan` (`/avouch:scan [project-dir]`): drafts or updates a project's ontology from its design documents. The result is a candidate. `avouch check` must exit 0, and each source gap is listed with a proposed fix to the document. When most objects have no documented field list, it also writes `avouch/doc-patch.md`: field lists, foreign keys and store names derived from the code, for a human to review and merge into the documents. It never drafts states, permissions or rules from code.
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
Before 0.1.3 the two skills were named `avouch-cli` and `aquery-scan`.
|
|
136
|
+
|
|
137
|
+
The skills run the CLI with `npx -y @githonllc/avouch@<version>`, pinned to the version of this repository. Set `AVOUCH_CLI` to a built `dist/cli/avouch.js` to use a local build instead. The `prepare` script builds `dist/` when npm installs the package from git (`npx -y github:githonllc/avouch`). `skills/scan/fixture/` is the test project for the skill.
|
|
136
138
|
|
|
137
139
|
## Publishing
|
|
138
140
|
|
|
@@ -149,6 +151,10 @@ A release needs the explicit approval of the project owner. Releases are publish
|
|
|
149
151
|
|
|
150
152
|
Version 0.1.0 was published by hand, because npm accepts a trusted publisher only for a package that exists.
|
|
151
153
|
|
|
154
|
+
CI and the release workflow run `scripts/scan-private.mjs`. It scans the path and text of each tracked file, and the commit messages of a pull request, for private content: local paths, tokens, private keys, and the patterns in the repository secret `PRIVATE_PATTERNS`. A hit shows only the file and line (or commit) and a pattern label, never the text. Pull requests from forks do not get the secret, so they run with the built-in patterns only. Pushes to `main`, pull requests from this repository and releases fail if the secret is missing.
|
|
155
|
+
|
|
156
|
+
The secret is generated from a list kept in a private repository; maintainers never edit it by hand. After the list changes, a maintainer runs the generator, which prints a pattern count and a sha256 fingerprint, and re-sets the secret with `gh secret set PRIVATE_PATTERNS`. The scan prints `PRIVATE_PATTERNS: <n> patterns, sha256 <12 hex>` for the patterns it loaded (comment lines dropped, pattern lines joined by `\n`); the next CI run on `main` must show the same count and fingerprint as the generator.
|
|
157
|
+
|
|
152
158
|
## Versions and identifiers
|
|
153
159
|
|
|
154
160
|
The ontology file has `formatVersion: 1`. The schema `$id` is `https://w3id.org/avouch/v1/schema.json`. The version rules are in SPEC section 8.
|
package/SPEC.md
CHANGED
|
@@ -49,7 +49,7 @@ The definitions quoted in the "Palantir analog" column are short quotations from
|
|
|
49
49
|
| `actionTypes.<A>.parameters` | foundation | action parameter: "Parameters are the inputs of an action type." | name → `{cite, type?, optional?, values?}`; 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; R1, R2 |
|
|
50
50
|
| `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 |
|
|
51
51
|
| `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 |
|
|
52
|
-
| `edits` / `creates` | foundation | action rules: modify / create object | |
|
|
52
|
+
| `edits` / `creates` / `deletes` | foundation | action rules: modify / create / delete object | `deletes` is a hard delete of datasource rows; a soft delete is an `edits` |
|
|
53
53
|
| `link_effects` | foundation + constraint | action rules: create / delete link | every incident link needs an entry, including `none` / `unspecified` (R6 sweep) |
|
|
54
54
|
| `emits` | extension | none (Palantir side effects are notifications / webhooks after submission) | events written in the same operation; R7, R8 |
|
|
55
55
|
| `permission` | extension | roles / submission criteria on the current user are approximate. Forms: `unknown`, `{keys, cite, …}`, `{none, cite}` (optionally with `principals`), `{any_of: [keys-form, …]}` (OR of keys-forms, at least 2) | R1, R10 |
|
|
@@ -110,11 +110,11 @@ The adapter may read the ontology instance (for example to list action ids in a
|
|
|
110
110
|
| 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 |
|
|
111
111
|
| 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 |
|
|
112
112
|
| 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 |
|
|
113
|
-
| 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>`) 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 |
|
|
113
|
+
| 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 |
|
|
114
114
|
| R8 | emitted events are in the catalog | `eventCatalog` | | |
|
|
115
115
|
| R9 | scenario cross references | `scenarios` | every action is cited; with `config.scenarioCoverage: "enforced"` (default `"printed"`, coverage only printed) every condition without `scenarios` fails `criterion_uncovered` (key `<ID>:<condition id>`) and every non-`none`, non-`unspecified` link effect without `scenarios` fails `effect_uncovered` (key `<ID>:<link id>`); `unspecified` effects are not reported again, R6 reports them | with `printed`, that scenarios cover every condition; that a listed scenario supports the claim |
|
|
116
116
|
| R10 | permission key sets equal the source rows (for `any_of`: the union over alternatives of keys ∪ conditional_keys; each alternative's cite must also name its own keys); cites name the required source | `permissions`, `config.requiredSource.R10` | keys match the source | |
|
|
117
|
-
| R11 | an action that edits an input lists the materialized property | none | declaration is complete | same-operation atomicity of the committed effect (one invocation may have several attempts), which the evidence adapter guarantees |
|
|
117
|
+
| R11 | an action that edits an input, creates an input's object, or deletes an input's object (unless it also deletes the materialized property's own object, whose row is gone) lists the materialized property. An action that deletes the property's own object and also edits or creates an input is still reported, because the edited inputs may belong to other rows; waive it with a reason when they cannot. | none | declaration is complete | same-operation atomicity of the committed effect (one invocation may have several attempts), which the evidence adapter guarantees |
|
|
118
118
|
| R12 | decision tables, all waivable: an action with conditions has a `decision` (`decision_table_missing`, key `<ID>`); a table has `otherwise` (`otherwise_missing`); a row does not use a condition without expression, or one that reaches a derived property without expression (`condition_unspecified`, key `<ID>:<condition id>`) (implementation decision: like the R4 and literal checks, the derived-property part is computed only for conditions whose expression passes the R1 type check); rows with different results have `order` (`row_order_uncited`); for rows `i < j` with different results whose position cites are in one section, the position of `i` comes first (`row_order_mismatch`, key `<ID>:rows.<i>:rows.<j>`, once per pair; implementation decision, since facts carry no order across sections: a row's position cite is in the section of the first listed cite whose quote names its result, and its position is the earliest such quote in that section). A row whose position-cite section is `missing` or `ambiguous` has no position and is not compared. Rows whose position cites are in different sections are not compared, even if they also cite a shared section; ordering across sections relies on `decision.order` and scenarios with overlapping inputs; each row names its result in a cite quote (`result_unsupported`, key `<ID>:rows.<i>` or `<ID>:otherwise`); `next: {unspecified}` is reported (`next_unspecified`); a row `passes(c)` where c may be UNKNOWN, no earlier row is `fails(c)`, and a later row up to the first later `fails(c)` row (inclusive), or `otherwise` when there is no such row, has class `applied` or `pending` (`unsafe_fallthrough`, key `<ID>:rows.<i>`); the body of an `exists` / `none` / `all` may be UNKNOWN (`quantifier_absorbs_unknown`, key `<ID>:<condition id>:<path>` or `derivedProperties.<id>:<path>`, path as in type errors, for example `$.and[1].none`); both see section 9, Static UNKNOWN analysis | `section`, `fieldList` | every row has a source for its result and, within one section, for its place | that rows do not overlap; that the order cite covers the whole table |
|
|
119
119
|
|
|
120
120
|
## 6. Rule switches
|
|
@@ -146,7 +146,7 @@ The table lists only the optional facts. Every rule may also read the required f
|
|
|
146
146
|
|
|
147
147
|
## 8. Versions
|
|
148
148
|
|
|
149
|
-
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.
|
|
149
|
+
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.
|
|
150
150
|
During 0.x the v1 draft changes in place (`formatVersion` 1, `$id` v1); the rule above applies from the 1.0 semantic freeze.
|
|
151
151
|
|
|
152
152
|
## 9. Decision semantics
|
package/dist/src/checker.js
CHANGED
|
@@ -261,6 +261,8 @@ export function check(ontology, facts) {
|
|
|
261
261
|
v("R1", p, `${f} must be a list`);
|
|
262
262
|
if (c.creates !== undefined && !Array.isArray(c.creates))
|
|
263
263
|
v("R1", p, "creates must be a list");
|
|
264
|
+
if (c.deletes !== undefined && !Array.isArray(c.deletes))
|
|
265
|
+
v("R1", p, "deletes must be a list");
|
|
264
266
|
if (!isObj(c.link_effects))
|
|
265
267
|
v("R1", p, "link_effects missing (use {})");
|
|
266
268
|
if (c.crossContext !== undefined) {
|
|
@@ -322,6 +324,9 @@ export function check(ontology, facts) {
|
|
|
322
324
|
for (const o of arr(c.creates))
|
|
323
325
|
if (!objType(o) && !outOfScope.has(o))
|
|
324
326
|
v("R1", p, `creates ${o}: unknown object`);
|
|
327
|
+
for (const o of arr(c.deletes))
|
|
328
|
+
if (!objType(o) && !outOfScope.has(o))
|
|
329
|
+
v("R1", p, `deletes ${o}: unknown object`);
|
|
325
330
|
for (const e of Object.keys(isObj(c.edit_cites) ? c.edit_cites : {}))
|
|
326
331
|
if (!arr(c.edits).includes(e) || !isObj(c.edit_cites[e]?.cite))
|
|
327
332
|
v("R1", `${p}.edit_cites.${e}`, "must name an edit and carry a cite");
|
|
@@ -633,7 +638,7 @@ export function check(ontology, facts) {
|
|
|
633
638
|
if (!entities.has(x))
|
|
634
639
|
v("R5", `entity:${x}`, `outOfScopeObjectTypes.${x} is not a source entity`, "out_of_scope_unknown");
|
|
635
640
|
}
|
|
636
|
-
const touched = (c) => new Set([...arr(c.edits).map((e) => e.split(".")[0]), ...arr(c.creates)].filter(objType));
|
|
641
|
+
const touched = (c) => new Set([...arr(c.edits).map((e) => e.split(".")[0]), ...arr(c.creates), ...arr(c.deletes)].filter(objType));
|
|
637
642
|
for (const [cid, c] of Object.entries(commands)) {
|
|
638
643
|
if (!isObj(c))
|
|
639
644
|
continue;
|
|
@@ -713,7 +718,7 @@ export function check(ontology, facts) {
|
|
|
713
718
|
// columns changed on existing rows (per store; checked per object), and the events written
|
|
714
719
|
const observe = (id) => {
|
|
715
720
|
const committed = part(id).committed;
|
|
716
|
-
const rowStores = new Set(), insertedStores = new Set(), insDelStores = new Set(), events = new Set();
|
|
721
|
+
const rowStores = new Set(), insertedStores = new Set(), insDelStores = new Set(), deletedStores = new Set(), events = new Set();
|
|
717
722
|
const changedCols = new Map();
|
|
718
723
|
for (const i of committed) {
|
|
719
724
|
for (const e of i.delta.events)
|
|
@@ -723,6 +728,8 @@ export function check(ontology, facts) {
|
|
|
723
728
|
rowStores.add(tb);
|
|
724
729
|
if (d.inserted.length)
|
|
725
730
|
insertedStores.add(tb);
|
|
731
|
+
if (d.deleted.length)
|
|
732
|
+
deletedStores.add(tb);
|
|
726
733
|
if (d.inserted.length || d.deleted.length)
|
|
727
734
|
insDelStores.add(tb);
|
|
728
735
|
for (const u of d.updated)
|
|
@@ -730,7 +737,7 @@ export function check(ontology, facts) {
|
|
|
730
737
|
changedCols.set(tb, (changedCols.get(tb) ?? new Set()).add(col));
|
|
731
738
|
}
|
|
732
739
|
}
|
|
733
|
-
return { committed, rowStores, insertedStores, insDelStores, changedCols, events };
|
|
740
|
+
return { committed, rowStores, insertedStores, insDelStores, deletedStores, changedCols, events };
|
|
734
741
|
};
|
|
735
742
|
for (const [cid, c] of Object.entries(commands)) {
|
|
736
743
|
if (!isObj(c))
|
|
@@ -783,14 +790,14 @@ export function check(ontology, facts) {
|
|
|
783
790
|
v("R7", cid, "the profile requires evidence, but the action declares none", "evidence_missing");
|
|
784
791
|
if (facts.evidence !== undefined) {
|
|
785
792
|
// a pure link table (not any object's table) may be written when the action declares an `effect` on that link;
|
|
786
|
-
// an object's table still needs the object in edits / creates
|
|
793
|
+
// an object's table still needs the object in edits / creates / deletes
|
|
787
794
|
const objTables = new Set(Object.values(objects).map((o) => o?.datasource));
|
|
788
795
|
for (const [cid, c] of Object.entries(commands)) {
|
|
789
796
|
if (!isObj(c) || c.evidence === undefined)
|
|
790
797
|
continue;
|
|
791
798
|
claimed.add(c.evidence);
|
|
792
799
|
const { rejected } = part(c.evidence);
|
|
793
|
-
const { committed, rowStores, insertedStores, insDelStores, changedCols, events } = observe(c.evidence);
|
|
800
|
+
const { committed, rowStores, insertedStores, insDelStores, deletedStores, changedCols, events } = observe(c.evidence);
|
|
794
801
|
const attempts = [...committed, ...rejected].reduce((n, i) => n + i.rolledBackAttempts, 0); // malformed records add nothing
|
|
795
802
|
info.push(`R7 ${cid} (${c.evidence}, ${committed.length} committed, ${rejected.length} rejected invocations, ${attempts} rolled-back attempts): changed {${[...rowStores].sort()}} events {${[...events].sort()}}`);
|
|
796
803
|
if (!committed.length) {
|
|
@@ -804,7 +811,7 @@ export function check(ontology, facts) {
|
|
|
804
811
|
const allowed = new Set([...declared, ...linkTables, ...infra]);
|
|
805
812
|
for (const tb of rowStores)
|
|
806
813
|
if (!allowed.has(tb))
|
|
807
|
-
v("R7", `${cid}:${tb}`, `evidence (${c.evidence}) changed store ${tb}, not declared by edits/creates`, "table_undeclared");
|
|
814
|
+
v("R7", `${cid}:${tb}`, `evidence (${c.evidence}) changed store ${tb}, not declared by edits/creates/deletes`, "table_undeclared");
|
|
808
815
|
for (const e of events)
|
|
809
816
|
if (!arr(c.emits).includes(e))
|
|
810
817
|
v("R7", `${cid}:event:${e}`, `evidence (${c.evidence}) wrote event ${e}, not in emits`, "event_undeclared");
|
|
@@ -831,6 +838,9 @@ export function check(ontology, facts) {
|
|
|
831
838
|
for (const o of arr(c.creates))
|
|
832
839
|
if (objType(o) && !insertedStores.has(objects[o].datasource))
|
|
833
840
|
unwitnessed(`creates:${o}`, `inserted a row into ${objects[o].datasource}`);
|
|
841
|
+
for (const o of arr(c.deletes))
|
|
842
|
+
if (objType(o) && !deletedStores.has(objects[o].datasource))
|
|
843
|
+
unwitnessed(`deletes:${o}`, `deleted a row of ${objects[o].datasource}`);
|
|
834
844
|
for (const l of links.filter(pureLink))
|
|
835
845
|
if (!insDelStores.has(l.table))
|
|
836
846
|
unwitnessed(`link:${l.id}`, `inserted or deleted a row of the link store ${l.table}`);
|
|
@@ -967,7 +977,9 @@ export function check(ontology, facts) {
|
|
|
967
977
|
for (const [cid, c] of Object.entries(commands)) {
|
|
968
978
|
if (!isObj(c))
|
|
969
979
|
continue;
|
|
970
|
-
|
|
980
|
+
// deleting an input object writes it, unless the target's own object is deleted too (the target row is gone)
|
|
981
|
+
const hit = reads.filter((r) => arr(c.edits).includes(r) || arr(c.creates).includes(r.split(".")[0])
|
|
982
|
+
|| (arr(c.deletes).includes(r.split(".")[0]) && !arr(c.deletes).includes(name)));
|
|
971
983
|
if (hit.length && !arr(c.edits).includes(target))
|
|
972
984
|
v("R11", `${cid}:${target}`, `${cid} writes ${hit.join(", ")} (inputs of ${target}) but does not list ${target} in edits`, "materialization_missing");
|
|
973
985
|
}
|
package/dist/src/query.d.ts
CHANGED
|
@@ -162,6 +162,7 @@ export type QueryAction = {
|
|
|
162
162
|
cite: QueryCite | null;
|
|
163
163
|
}[];
|
|
164
164
|
creates: string[];
|
|
165
|
+
deletes: string[];
|
|
165
166
|
emits: string[];
|
|
166
167
|
linkEffects: {
|
|
167
168
|
link: string;
|
|
@@ -232,6 +233,7 @@ export type QueryObject = {
|
|
|
232
233
|
action: string;
|
|
233
234
|
edits: string[];
|
|
234
235
|
creates: boolean;
|
|
236
|
+
deletes: boolean;
|
|
235
237
|
}[];
|
|
236
238
|
readers: {
|
|
237
239
|
action: string;
|
|
@@ -261,6 +263,7 @@ export type QueryWrites = {
|
|
|
261
263
|
path: string[];
|
|
262
264
|
}[];
|
|
263
265
|
creators: string[];
|
|
266
|
+
deleters: string[];
|
|
264
267
|
};
|
|
265
268
|
export type QueryReads = {
|
|
266
269
|
target: string;
|
package/dist/src/query.js
CHANGED
|
@@ -344,6 +344,7 @@ export function queryAction(ont, id) {
|
|
|
344
344
|
decision: isObj(a.decision) ? { order: citeOf(map(a.decision.order).cite), rows: decisionRows(an, id, a) } : null,
|
|
345
345
|
edits: strs(a.edits).map((prop) => ({ prop, cite: citeOf(map(own(editCites, prop)).cite) })),
|
|
346
346
|
creates: strs(a.creates),
|
|
347
|
+
deletes: strs(a.deletes),
|
|
347
348
|
emits: strs(a.emits),
|
|
348
349
|
linkEffects: Object.entries(map(a.link_effects)).flatMap(([link, le]) => {
|
|
349
350
|
const e = map(le);
|
|
@@ -396,8 +397,9 @@ export function queryObject(ont, id) {
|
|
|
396
397
|
for (const [aid, a] of sortedActions(ont)) {
|
|
397
398
|
const edits = strs(a.edits).filter((e) => e.startsWith(prefix));
|
|
398
399
|
const creates = strs(a.creates).includes(id);
|
|
399
|
-
|
|
400
|
-
|
|
400
|
+
const deletes = strs(a.deletes).includes(id);
|
|
401
|
+
if (edits.length || creates || deletes)
|
|
402
|
+
writers.push({ action: aid, edits, creates, deletes });
|
|
401
403
|
}
|
|
402
404
|
if (o === undefined)
|
|
403
405
|
return { id, context: null, datasource: null, doc: null, outOfScope, scope: null, properties: [], derived: [], stateMachine: null, links: [], writers, readers: [], unanalyzed: [] };
|
|
@@ -494,7 +496,8 @@ export function queryWrites(ont, t) {
|
|
|
494
496
|
const acts = sortedActions(ont);
|
|
495
497
|
const writersOf = (prop, path) => acts.filter(([, a]) => strs(a.edits).includes(prop)).map(([aid, a]) => ({ action: aid, cite: citeOf(map(own(map(a.edit_cites), prop)).cite), path }));
|
|
496
498
|
if (tg.kind === "property")
|
|
497
|
-
return { target: t, kind: "property", unanalyzed: [], writers: writersOf(t, []), creators: acts.filter(([, a]) => strs(a.creates).includes(tg.obj)).map(([aid]) => aid)
|
|
499
|
+
return { target: t, kind: "property", unanalyzed: [], writers: writersOf(t, []), creators: acts.filter(([, a]) => strs(a.creates).includes(tg.obj)).map(([aid]) => aid),
|
|
500
|
+
deleters: acts.filter(([, a]) => strs(a.deletes).includes(tg.obj)).map(([aid]) => aid) };
|
|
498
501
|
// Breadth first from the derived target: each input once, by its shortest path.
|
|
499
502
|
const an = new Analysis(ont);
|
|
500
503
|
const writers = [];
|
|
@@ -518,7 +521,7 @@ export function queryWrites(ont, t) {
|
|
|
518
521
|
frontier = next;
|
|
519
522
|
}
|
|
520
523
|
writers.sort((a, b) => a.path.length - b.path.length || sortStr(a.path.join(), b.path.join()) || sortStr(a.action, b.action));
|
|
521
|
-
return { target: t, kind: "derived", unanalyzed: derivedErrors(an, seenDerived), writers, creators: [] };
|
|
524
|
+
return { target: t, kind: "derived", unanalyzed: derivedErrors(an, seenDerived), writers, creators: [], deleters: [] };
|
|
522
525
|
}
|
|
523
526
|
export function queryReads(ont, t) {
|
|
524
527
|
const tg = target(ont, t);
|
package/ontology.schema.json
CHANGED
|
@@ -689,6 +689,13 @@
|
|
|
689
689
|
},
|
|
690
690
|
"description": "Objects the action creates (optional). Foundation (closest analog: the create-object rules of a Palantir action type). R1: each must be a key of `objectTypes` or of `outOfScopeObjectTypes`. Declared objects here are touched (R5, R6, R7) and count as writing every property of that object for R11. R5: creating the `from` object of a link with `create_rule` triggers that rule."
|
|
691
691
|
},
|
|
692
|
+
"deletes": {
|
|
693
|
+
"type": "array",
|
|
694
|
+
"items": {
|
|
695
|
+
"type": "string"
|
|
696
|
+
},
|
|
697
|
+
"description": "Objects whose rows the action removes (optional). Foundation (closest analog: the delete-object rules of a Palantir action type). R1: each must be a key of `objectTypes` or of `outOfScopeObjectTypes`. A soft delete is an `edits` of the marker property. Declared objects here are touched (R5, R6, R7). R7: for an action with `evidence`, each declared object here must have a row of its datasource deleted by at least one committed invocation (else `effect_unwitnessed`, key `<ID>:deletes:<Object>`); an upsert declares `creates`, not `deletes`. R11: deleting an input of a `materializedFrom` property counts as writing it, unless the object of that property is also listed here."
|
|
698
|
+
},
|
|
692
699
|
"edit_cites": {
|
|
693
700
|
"type": "object",
|
|
694
701
|
"propertyNames": {
|