avouch 0.1.22
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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +356 -0
- package/SPEC.md +379 -0
- package/dist/cli/avouch.d.ts +2 -0
- package/dist/cli/avouch.js +68 -0
- package/dist/cli/check.d.ts +86 -0
- package/dist/cli/check.js +333 -0
- package/dist/cli/facts.d.ts +3 -0
- package/dist/cli/facts.js +157 -0
- package/dist/cli/gate.d.ts +3 -0
- package/dist/cli/gate.js +149 -0
- package/dist/cli/init.d.ts +21 -0
- package/dist/cli/init.js +282 -0
- package/dist/cli/jsonl.d.ts +5 -0
- package/dist/cli/jsonl.js +20 -0
- package/dist/cli/query.d.ts +2 -0
- package/dist/cli/query.js +138 -0
- package/dist/cli/reconcile.d.ts +9 -0
- package/dist/cli/reconcile.js +131 -0
- package/dist/src/checker.d.ts +3 -0
- package/dist/src/checker.js +1136 -0
- package/dist/src/contract.d.ts +135 -0
- package/dist/src/contract.js +3 -0
- package/dist/src/coverage.d.ts +65 -0
- package/dist/src/coverage.js +237 -0
- package/dist/src/evaluate.d.ts +66 -0
- package/dist/src/evaluate.js +535 -0
- package/dist/src/expr.d.ts +209 -0
- package/dist/src/expr.js +497 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +11 -0
- package/dist/src/ledger.d.ts +96 -0
- package/dist/src/ledger.js +241 -0
- package/dist/src/markdown.d.ts +66 -0
- package/dist/src/markdown.js +701 -0
- package/dist/src/patch.d.ts +6 -0
- package/dist/src/patch.js +37 -0
- package/dist/src/query.d.ts +333 -0
- package/dist/src/query.js +739 -0
- package/dist/src/reconcile.d.ts +61 -0
- package/dist/src/reconcile.js +151 -0
- package/dist/src/stage1-check.d.ts +31 -0
- package/dist/src/stage1-check.js +329 -0
- package/dist/src/touched.d.ts +1 -0
- package/dist/src/touched.js +9 -0
- package/dist/src/unknown.d.ts +10 -0
- package/dist/src/unknown.js +173 -0
- package/docs/adapter-guide.md +187 -0
- package/docs/markdown-convention.md +635 -0
- package/ontology.schema.json +1757 -0
- package/package.json +57 -0
- package/templates/agents-block.md +12 -0
- package/vocab.ttl +524 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { navType, objectBinding, own, resolvesInDomain } from "./expr.js";
|
|
2
|
+
export const MAX_NULLABLE_REFS = 8;
|
|
3
|
+
class NoExpression extends Error {
|
|
4
|
+
}
|
|
5
|
+
// One domain for values and predicates: a null value is {U}, a non-null value is {T, F}.
|
|
6
|
+
const U = new Set(["U"]);
|
|
7
|
+
const TF = new Set(["T", "F"]);
|
|
8
|
+
const QUANTIFIERS = new Set(["exists", "none", "all"]);
|
|
9
|
+
const nodeKey = (e) => Object.keys(e)[0];
|
|
10
|
+
const operands = (e) => e[nodeKey(e)];
|
|
11
|
+
// `x.prop` -> ["x", "prop"]; `x` -> ["x", undefined]
|
|
12
|
+
function split(path) {
|
|
13
|
+
const dot = path.indexOf(".");
|
|
14
|
+
return dot < 0 ? [path, undefined] : [path.slice(0, dot), path.slice(dot + 1)];
|
|
15
|
+
}
|
|
16
|
+
// A path with the head `self` replaced by the current self (an object binding name, or "self" at the root).
|
|
17
|
+
function norm(path, self) {
|
|
18
|
+
const [head, prop] = split(path);
|
|
19
|
+
return head !== "self" ? path : prop === undefined ? self : `${self}.${prop}`;
|
|
20
|
+
}
|
|
21
|
+
const and3 = (a, b) => (a === "F" || b === "F" ? "F" : a === "U" || b === "U" ? "U" : "T");
|
|
22
|
+
const or3 = (a, b) => (a === "T" || b === "T" ? "T" : a === "U" || b === "U" ? "U" : "F");
|
|
23
|
+
const not3 = (a) => (a === "T" ? "F" : a === "F" ? "T" : "U");
|
|
24
|
+
// a Kleene connective lifted to sets: the product of the operand sets
|
|
25
|
+
function lift(xs, ys, f) {
|
|
26
|
+
const out = new Set();
|
|
27
|
+
for (const a of xs)
|
|
28
|
+
for (const b of ys)
|
|
29
|
+
out.add(f(a, b));
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
// The abstract result of e for one combination: isNull says which normalized paths are null. Every operand of every node
|
|
33
|
+
// is evaluated (no short-circuit), and only isNull(of) of a derived property prunes: freeNullableRefs relies on both.
|
|
34
|
+
function ev(ont, e, self, isNull) {
|
|
35
|
+
const k = nodeKey(e);
|
|
36
|
+
switch (k) {
|
|
37
|
+
case "lit":
|
|
38
|
+
return TF;
|
|
39
|
+
case "ref":
|
|
40
|
+
return isNull(norm(e.ref, self)) ? U : TF;
|
|
41
|
+
case "derived": {
|
|
42
|
+
// the only place a derived property is inlined: null without reading the expression when its `of` is null
|
|
43
|
+
const d = e.derived;
|
|
44
|
+
const of = norm(d.of.ref, self);
|
|
45
|
+
if (isNull(of))
|
|
46
|
+
return U;
|
|
47
|
+
const x = own(ont.derivedProperties, d.id)?.expr;
|
|
48
|
+
if (x === undefined)
|
|
49
|
+
throw new NoExpression(d.id);
|
|
50
|
+
return ev(ont, x, of, isNull);
|
|
51
|
+
}
|
|
52
|
+
case "exists":
|
|
53
|
+
case "none":
|
|
54
|
+
case "all":
|
|
55
|
+
return TF; // two-valued; the body is analysed on its own
|
|
56
|
+
case "and":
|
|
57
|
+
case "or": {
|
|
58
|
+
const [first, ...rest] = operands(e).map((x) => ev(ont, x, self, isNull));
|
|
59
|
+
return rest.reduce((acc, p) => lift(acc, p, k === "and" ? and3 : or3), first);
|
|
60
|
+
}
|
|
61
|
+
case "not":
|
|
62
|
+
return new Set([...ev(ont, operands(e)[0], self, isNull)].map(not3));
|
|
63
|
+
case "isNull":
|
|
64
|
+
case "isNotNull": {
|
|
65
|
+
const a = ev(ont, operands(e)[0], self, isNull);
|
|
66
|
+
const out = new Set();
|
|
67
|
+
if (a.has("U"))
|
|
68
|
+
out.add(k === "isNull" ? "T" : "F");
|
|
69
|
+
if (a.has("T") || a.has("F"))
|
|
70
|
+
out.add(k === "isNull" ? "F" : "T");
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
default: {
|
|
74
|
+
// strict nodes (comparisons, in, subsetOf, plus, dateIn): a null operand gives null; non-null operands give T or F
|
|
75
|
+
// (no narrowing by constants)
|
|
76
|
+
const xs = operands(e).map((x) => ev(ont, x, self, isNull));
|
|
77
|
+
const out = new Set();
|
|
78
|
+
if (xs.some((x) => x.has("U")))
|
|
79
|
+
out.add("U");
|
|
80
|
+
if (xs.every((x) => x.has("T") || x.has("F")))
|
|
81
|
+
(out.add("T"), out.add("F"));
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// The distinct normalized paths that may be null and that the outer evaluation reads, sorted. One run of ev with no path
|
|
87
|
+
// null reaches every read (ev evaluates every operand; only a null `of` prunes); quantifiers are not entered, and a
|
|
88
|
+
// `nav.from` is never a read.
|
|
89
|
+
export function freeNullableRefs(ont, e, mayNull) {
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
ev(ont, e, "self", (p) => (seen.add(p), false));
|
|
92
|
+
return [...seen].filter(mayNull).sort();
|
|
93
|
+
}
|
|
94
|
+
// The predicate results of e for one combination: isNull says which paths are null.
|
|
95
|
+
export function abstractEval(ont, e, isNull) {
|
|
96
|
+
return ev(ont, e, "self", isNull);
|
|
97
|
+
}
|
|
98
|
+
// Whether some combination of nulls over the free nullable reads of e may give U.
|
|
99
|
+
export function mayUnknown(ont, e, mayNull) {
|
|
100
|
+
const refs = freeNullableRefs(ont, e, mayNull);
|
|
101
|
+
if (refs.length > MAX_NULLABLE_REFS)
|
|
102
|
+
return true;
|
|
103
|
+
for (let mask = 0; mask < 2 ** refs.length; mask++) {
|
|
104
|
+
const nulls = new Set(refs.filter((_, i) => (mask >> i) & 1));
|
|
105
|
+
if (abstractEval(ont, e, (p) => nulls.has(p)).has("U"))
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
// Nullability for any context of one ontology and its facts; the field lists are read once per object type.
|
|
111
|
+
function nullability(ont, facts) {
|
|
112
|
+
const lists = new Map();
|
|
113
|
+
const fields = (objectType) => {
|
|
114
|
+
if (!lists.has(objectType)) {
|
|
115
|
+
const doc = own(ont.objectTypes, objectType)?.doc;
|
|
116
|
+
lists.set(objectType, typeof doc === "string" ? (facts.fieldList(doc, objectType)?.value ?? null) : null);
|
|
117
|
+
}
|
|
118
|
+
return lists.get(objectType);
|
|
119
|
+
};
|
|
120
|
+
return (ctx) => (path) => {
|
|
121
|
+
const [head, prop] = split(path);
|
|
122
|
+
if (head === "now" || head === "actor")
|
|
123
|
+
return false;
|
|
124
|
+
const local = head === "self" || Object.hasOwn(ctx.locals, head);
|
|
125
|
+
const param = local || ctx.action === undefined ? undefined : own(ont.actionTypes[ctx.action]?.parameters, head);
|
|
126
|
+
if (!local && param === undefined)
|
|
127
|
+
return true; // not reachable after the type check; conservative
|
|
128
|
+
if (param !== undefined && (param.optional === true || resolvesInDomain(param)))
|
|
129
|
+
return true; // an optional parameter or a ref with unresolved: domain, and every property read through it
|
|
130
|
+
if (prop === undefined)
|
|
131
|
+
return false; // an object binding, or a required parameter
|
|
132
|
+
const o = objectBinding(ont, head, ctx);
|
|
133
|
+
// non-null only when the field list says so; a projected property (from) may always be null (its key may be absent)
|
|
134
|
+
return o === undefined || own(own(ont.objectTypes, o)?.properties, prop)?.from !== undefined || fields(o)?.get(prop)?.nullable !== false;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Nullability of the paths in ctx. A field is non-null only when its field list says so; a field that is not listed,
|
|
138
|
+
// or an object without a field list, has unknown nullability and may be null. An optional parameter may be null; so
|
|
139
|
+
// may every property read through it. A ref parameter with `unresolved: domain` is the same.
|
|
140
|
+
export function mayNullIn(ont, facts, ctx) {
|
|
141
|
+
return nullability(ont, facts)(ctx);
|
|
142
|
+
}
|
|
143
|
+
// The quantifiers of e (preorder, through and / or / not and quantifier bodies) whose body may be UNKNOWN. Paths use
|
|
144
|
+
// the type-error notation: `$.none`, `$.and[1].none`, `$.all.where.and[0].exists`. Derived property nodes are not
|
|
145
|
+
// entered: a quantifier inside a derived property is reported once, for the derived property itself. A body that
|
|
146
|
+
// reaches a derived property without an expression is not analysed (its nested quantifiers still are).
|
|
147
|
+
export function absorbingQuantifiers(ont, facts, e, ctx) {
|
|
148
|
+
const out = [];
|
|
149
|
+
const mayNullFor = nullability(ont, facts);
|
|
150
|
+
const walk = (x, at, c) => {
|
|
151
|
+
const k = nodeKey(x);
|
|
152
|
+
const p = `${at}.${k}`;
|
|
153
|
+
if (k === "and" || k === "or" || k === "not")
|
|
154
|
+
operands(x).forEach((y, i) => walk(y, `${p}[${i}]`, c));
|
|
155
|
+
else if (QUANTIFIERS.has(k)) {
|
|
156
|
+
const q = x[k];
|
|
157
|
+
if (q.where === undefined)
|
|
158
|
+
return;
|
|
159
|
+
const c2 = { ...c, locals: { ...c.locals, [q.as]: navType(ont, q.in, c, `${p}.in`) } };
|
|
160
|
+
try {
|
|
161
|
+
if (mayUnknown(ont, q.where, mayNullFor(c2)))
|
|
162
|
+
out.push(p);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
if (!(err instanceof NoExpression))
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
walk(q.where, `${p}.where`, c2);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
walk(e, "$", ctx);
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Writing an Avouch adapter
|
|
2
|
+
|
|
3
|
+
This guide is for engineers who want to use Avouch for their own project. It tells you which facts the checker needs, how the toy library adapter supplies them, and how to write the profile.
|
|
4
|
+
|
|
5
|
+
## For AI agents
|
|
6
|
+
|
|
7
|
+
Read this when `avouch check` or `avouch facts` reports `no adapter`, or when `/avouch:scan` stops because no adapter can read the documents. An adapter reads the design documents and gives the checker the facts that the ontology must agree with.
|
|
8
|
+
|
|
9
|
+
1. The documents are Markdown: you do not need code. Use the built-in Markdown adapter. Add this key to `avouch.config.json` at the git top level, with the paths of the design documents (relative to the file):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{ "adapter": { "markdown": { "docs": ["docs/design.md"] } } }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then run `npx -y avouch facts`. Exit code 0 means the adapter reads the documents. Rules that need a structured fact (a state machine, a permission table, a catalog) need sections in the grammar of `docs/markdown-convention.md`, and a `facts` anchor for each (section 7). Without them, those rules report `fact_missing`: disable each one with a reason (`"rules": {"R8": {"disabled": "<reason>"}}` in `profile`), or rewrite the section to the convention.
|
|
16
|
+
|
|
17
|
+
2. The documents are in another form, or you need structured facts from Markdown that does not follow the convention: write an adapter module. Do this in a new session, not in the session that writes or edits the ontology. Give the new session this prompt:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
Write an Avouch adapter module for this repository.
|
|
21
|
+
Read first: https://github.com/githonllc/avouch/blob/main/docs/adapter-guide.md (sections 2, 3 and "Writing your own adapter"),
|
|
22
|
+
src/contract.ts (the SourceFacts interface) and examples/library/adapter.ts of the avouch package.
|
|
23
|
+
Inputs: the design documents only. Do not read the ontology (*.ontology.yaml) and do not run avouch query.
|
|
24
|
+
Output: avouch/adapter.mjs, which exports facts(ontologyPath) and returns SourceFacts. Parse the documents at run time.
|
|
25
|
+
Put no expected value (state, edge, trigger, key, event, scenario or catalog name) in the code.
|
|
26
|
+
Set "adapter": "avouch/adapter.mjs" in avouch.config.json. Add a test that runs facts() on the real documents.
|
|
27
|
+
Done when the test passes and `npx -y avouch check` no longer reports "no adapter".
|
|
28
|
+
Report each document form that the adapter does not read.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The reason for a new session: the adapter gives the expected facts, and the ontology declares them. When one agent writes both, `avouch check` compares the agent with itself, and a PASS proves nothing. Not guaranteed: the checker cannot see which session wrote the adapter. Review the adapter in the pull request.
|
|
32
|
+
|
|
33
|
+
## 1. The three layers
|
|
34
|
+
|
|
35
|
+
Avouch has three layers: the format layer (this package), the project profile and adapter (your code), and the ontology instance (your `<project>.ontology.yaml`). SPEC section 1 defines them. The format layer never reads a source document. Your adapter reads your sources and returns `SourceFacts`. The checker compares your ontology with those facts.
|
|
36
|
+
|
|
37
|
+
## 2. The fact contract
|
|
38
|
+
|
|
39
|
+
The contract is the `SourceFacts` interface in `src/contract.ts`. Your adapter returns one object of this type.
|
|
40
|
+
|
|
41
|
+
| Field | Required | Used by |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `config` | yes | all rules: the profile (section 5) |
|
|
44
|
+
| `diagnostics` | yes | your adapter's findings (parse errors, duplicate rows); reported and waived like violations |
|
|
45
|
+
| `info` | yes | lines that the checker prints |
|
|
46
|
+
| `section(anchor)` | yes | R2: finds a section by its anchor; returns the section, `"missing"` or `"ambiguous"` |
|
|
47
|
+
| `containsTerm(term)` | yes | R2: is the term in the source |
|
|
48
|
+
| `fieldList(anchor, objectType)` | yes | R2 (R6 and R12 also read it): the fields of an object in a section, each with `nullable`, or `null` |
|
|
49
|
+
| `storeCatalog` | no | R2 |
|
|
50
|
+
| `stateMachines` | no | R3: object type to the state machine at its authoritative anchor |
|
|
51
|
+
| `transitionTriggers` | no | R3: object type to `"FROM->TO"` to action ids |
|
|
52
|
+
| `contextMembers` | no | R5: context to member names |
|
|
53
|
+
| `eventCatalog` | no | R8 |
|
|
54
|
+
| `scenarios` | no | R9: scenario id to the cited action ids |
|
|
55
|
+
| `permissions` | no | R10 |
|
|
56
|
+
| `evidence` | no | R1 (id validity), R6 (`none_but_written`), R7 |
|
|
57
|
+
|
|
58
|
+
`permissions.quoteNamesKey(quote, key)` is the fourth function that you must supply when you supply `permissions`. R10 uses it to ask: does this quote name this key in the key syntax of your source?
|
|
59
|
+
|
|
60
|
+
Each optional field can be absent. When a rule that needs an absent field is enabled, the checker reports one `fact_missing` violation and skips the checks that depend on the fact. The table in SPEC section 6 lists the checks that are skipped for each fact.
|
|
61
|
+
|
|
62
|
+
## 3. Walkthrough: the toy library adapter
|
|
63
|
+
|
|
64
|
+
`examples/library/adapter.ts` builds `SourceFacts` for `examples/library/library.md`.
|
|
65
|
+
|
|
66
|
+
- It parses the `## L<n> <title>` headings into sections. A heading anchor that occurs two times gives `"ambiguous"`.
|
|
67
|
+
- `fieldList` finds the one `text` code block in the section whose first line is the object name, and reads its `- <field>` lines. A trailing `?` marks the field nullable and is not part of its name. A field that is not listed, or an object without a field list, has unknown nullability.
|
|
68
|
+
- `containsTerm` searches the document for the term.
|
|
69
|
+
- Before it extracts any fact, it removes every generated block (`<!-- BEGIN GENERATED: <name> -->` to `<!-- END GENERATED: <name> -->`, marker lines included); markers that do not pair throw. No fact may come from text generated from the ontology (SPEC section 4). This stripper is simplified: it does not skip fenced code blocks and does not accept annotated markers. A real adapter must handle the marker syntax of its own generator.
|
|
70
|
+
- All other facts are literals with an anchor: the store catalog (`L8`), the state machines (`L4`, `L5`), the transition triggers (`L6`), the context members (`L7`), the event catalog (`L9`) and the permissions (`L10`). The scenarios are a literal without an anchor.
|
|
71
|
+
|
|
72
|
+
The toy adapter writes these facts as literals to keep the example small. A real adapter must parse them from its sources, so that a change in a source document shows as a violation.
|
|
73
|
+
|
|
74
|
+
`examples/library/evidence.ts` supplies the execution evidence: for each evidence id, a list of invocation records (SPEC section 4). Each record has a harness-generated id, the outcome, a snapshot reference, the evaluation inputs and, when committed, the net change per store row by row (keyed by a stable row identity) and the events written. The checker derives the changed stores and the committed count from the records. Always set the record id: a record without one is keyed by its list position, and that key is not stable for waivers. Do not reuse a row identity within one invocation (SPEC section 4).
|
|
75
|
+
|
|
76
|
+
### Writing your own adapter
|
|
77
|
+
|
|
78
|
+
A project that does not write its documents to the Markdown convention (`docs/markdown-convention.md`) writes its own adapter. Follow these rules:
|
|
79
|
+
|
|
80
|
+
- Derive every fact from the source text. Parse it at run time, so that a change in a source document shows as a violation.
|
|
81
|
+
- Put no expected value in the adapter code: no state, edge, trigger, key, event, scenario or catalog name. A value in code is a value that the check cannot compare with the documents. The toy adapter uses literals only to keep the example small (see above).
|
|
82
|
+
- Take no fact from text generated from the ontology (SPEC section 4). Remove generated text before you extract any fact.
|
|
83
|
+
|
|
84
|
+
Not guaranteed: the checker cannot check these rules. It sees the facts that the adapter returns, not how the adapter made them. Review the adapter in the pull request, as you review the ledger.
|
|
85
|
+
|
|
86
|
+
## 4. Rules that need no project facts
|
|
87
|
+
|
|
88
|
+
R1, R4 and R11 never report `fact_missing` (SPEC section 6). R4 and R11 read only the ontology. R1 reads the ontology and the profile; when an action names `evidence`, R1 also needs the `evidence` fact to resolve it.
|
|
89
|
+
|
|
90
|
+
Parameters and parameter and actor reads (`parameters`, `{param}`, `{actor}`) need no new fact. For these reads, R1 and R4 use only the ontology. R2 checks parameter quotes with the same `section` function as all other quotes.
|
|
91
|
+
|
|
92
|
+
## 5. The profile
|
|
93
|
+
|
|
94
|
+
`examples/library/profile.ts` is the toy profile. It is a `FormatConfig`:
|
|
95
|
+
|
|
96
|
+
- `rules`: one switch for each rule R1–R12, `true` or `{ disabled: "<reason>" }`. Write all of them. A missing entry gives `rule_not_configured`.
|
|
97
|
+
- `requiredSource`: the anchor that each permission cite must name (R10).
|
|
98
|
+
- `scenarioCoverage`: `"printed"` (default) or `"enforced"` (R9). The toy profile uses the default.
|
|
99
|
+
- `crossContextMechanisms`: the legal `via` values of `crossContext` (R5).
|
|
100
|
+
- `actionIdPattern`: the pattern of an action id (R1).
|
|
101
|
+
- `linkFieldPattern`: the pattern of a field that holds a link (R6).
|
|
102
|
+
- `stateProperty`: the default name of the state property (R1, R3). An ontology machine names another property of its object with `stateMachines.<Object>.property`.
|
|
103
|
+
|
|
104
|
+
A harness that builds its own snapshot, and not through `projectSnapshot` (for example the input of the `reconcile` CLI, which is taken as given), must set each projected property (`properties.<p>.from`) the same way: from the top-level key of the JSON object in its column, null when the column is null or the key is absent or null, never decoded, and not set when the column is absent (SPEC section 9, `projectSnapshot` step 2).
|
|
105
|
+
|
|
106
|
+
## 6. Run the example
|
|
107
|
+
|
|
108
|
+
In the package directory, run:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
npm install
|
|
112
|
+
npm test
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 7. The built-in Markdown adapter
|
|
116
|
+
|
|
117
|
+
A project whose specification is plain Markdown can run `avouch check` without writing an adapter. `src/markdown.ts` is a generic adapter: `parseMarkdownConfig(text, file)` reads a JSON configuration (`validateMarkdownConfig(value, where)` checks an object that is already parsed), and `markdownFacts(docs, config)` turns document texts into `SourceFacts`. It reads no file; the command line reads the configuration and the documents.
|
|
118
|
+
|
|
119
|
+
### Configuration
|
|
120
|
+
|
|
121
|
+
The configuration is either the `adapter.markdown` object of the project configuration `avouch.config.json` (README, "Configuration"), and `docs` is then relative to that file, or a stand-alone `.json` file. A stand-alone file is named by `--adapter <file>.json`, or by the string `"adapter": "<file>.json"` in `avouch.config.json` (a path relative to that file); its `docs` are relative to the stand-alone file. `avouch check` without `--adapter` uses the `adapter` of `avouch.config.json`. Any other `--adapter` value, or any other string in `adapter`, is loaded as a module. The configuration for the toy library (in the directory of `library.md`; as a stand-alone file, or as the value of `adapter.markdown`) is:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"docs": ["library.md"],
|
|
126
|
+
"facts": { "storeCatalog": "L8", "eventCatalog": "L9", "contextMembers": "L7", "permissions": "L10", "scenarios": "L11", "stateMachines": { "Book": "L4", "Loan": "L5" }, "transitionTriggers": "L6" },
|
|
127
|
+
"profile": { "crossContextMechanisms": ["same_transaction"] }
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- `docs` (required): the source documents, a non-empty list of distinct paths. A relative path resolves from the directory of the configuration file. Two entries that resolve to the same file, or a file that cannot be read, stop the command with exit code 2.
|
|
132
|
+
- `facts` (optional): the anchor of the section that holds each optional fact: `storeCatalog` (R2), `eventCatalog` (R8), `contextMembers` (R5), `permissions` (R10), `scenarios` (R9), `stateMachines` and `transitionTriggers` (R3). A fact without an anchor is not supplied. `stateMachines` is an object, not a string: each key is an object type and each value is the anchor of the section that holds the state diagram of that object (`"stateMachines": {"Book": "L4"}`). `stateMachines` and `transitionTriggers` are set together or not at all; one without the other is an error.
|
|
133
|
+
- `profile` (optional): `actionIdPattern` and `linkFieldPattern` (regular expression strings), `stateProperty`, `crossContextMechanisms`, `scenarioCoverage`, `idempotencyDeclarationRequired`, `evidenceRequired` and `rules`. A `rules` entry is `true` or `{"disabled": "<reason>"}` and replaces the default for that rule.
|
|
134
|
+
|
|
135
|
+
An unknown key, a wrong type or an invalid regular expression is an error `<file>: <json path>: <problem>` (exit code 2).
|
|
136
|
+
|
|
137
|
+
Defaults: `actionIdPattern` `^[A-Z][A-Z_]*$`, `linkFieldPattern` `_id$`, `stateProperty` `state`, `crossContextMechanisms` empty. `requiredSource` is `{R10: <permissions anchor>}` when `facts.permissions` is set, else empty; it cannot be set separately. `scenarioCoverage`, `idempotencyDeclarationRequired` and `evidenceRequired` are set only when given. Rules R1, R2, R4, R5, R6, R8, R10, R11 and R12 are on. R9 is on when `facts.scenarios` is set. R3 is on when `facts.stateMachines` and `facts.transitionTriggers` are set. Without its facts, R9 or R3 is off, like R7, because the adapter supplies no facts for the rule; the checker prints the reason:
|
|
138
|
+
|
|
139
|
+
| Rule | Reason |
|
|
140
|
+
|---|---|
|
|
141
|
+
| R3 | facts.stateMachines and facts.transitionTriggers are not set: the Markdown adapter reads state machines only from those sections |
|
|
142
|
+
| R7 | the Markdown adapter reads no execution evidence |
|
|
143
|
+
| R9 | facts.scenarios is not set: the Markdown adapter reads scenarios only from that section |
|
|
144
|
+
|
|
145
|
+
### Parse conventions
|
|
146
|
+
|
|
147
|
+
The grammar of the documents is in `docs/markdown-convention.md`: the general rules, and one section for each fact with its template, example and diagnostics.
|
|
148
|
+
|
|
149
|
+
### Evidence and R6
|
|
150
|
+
|
|
151
|
+
The Markdown adapter reads no evidence, so `evidence` is absent. R6 stays on, because its static checks (missing, unspecified and stray link effects, link catalog completeness, stray non-link fields) need only the field lists. The cost is one violation `R6 fact_missing facts.evidence`. Waive it in the ontology:
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
knownSourceGaps:
|
|
155
|
+
- id: NO-EVIDENCE
|
|
156
|
+
rule: R6
|
|
157
|
+
kind: fact_missing
|
|
158
|
+
keys: [facts.evidence]
|
|
159
|
+
doc_line: the sources are plain Markdown; there is no execution evidence
|
|
160
|
+
conflict: R6 none_but_written needs evidence
|
|
161
|
+
proposed: add an evidence adapter
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Or switch R6 off with `"rules": {"R6": {"disabled": "<reason>"}}`; this also turns off the link checks.
|
|
165
|
+
|
|
166
|
+
An action that names `evidence` fails R1 when no evidence is supplied. To use the Markdown facts with your own evidence, write a small adapter module that combines them:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
export const facts = () => ({ ...markdownFacts(docs, cfg), evidence });
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Query
|
|
173
|
+
|
|
174
|
+
Without `--check` and without `--adapter`, the check stamp of `avouch query` stays `not_run`. `avouch query ... --check` runs the same check as `avouch check` with the adapter of the project configuration, and `--adapter <file>.json` with that file.
|
|
175
|
+
|
|
176
|
+
### Check documents without an ontology
|
|
177
|
+
|
|
178
|
+
`avouch facts` runs this adapter over the documents of a configuration and prints the facts, the fields of each `text` block and the diagnostics, without reading an ontology file. See "facts" in `README.md`.
|
|
179
|
+
|
|
180
|
+
### The toy library with the generic adapter
|
|
181
|
+
|
|
182
|
+
`test/markdown.test.ts` runs the clean toy ontology and its 28 mutations through both adapters, with every `evidence` id removed. The findings are the same, except:
|
|
183
|
+
|
|
184
|
+
- the toy adapter also reports R7 and R6 `none_but_written` (the generic adapter has no evidence);
|
|
185
|
+
- the generic adapter also reports R6 `fact_missing facts.evidence`.
|
|
186
|
+
|
|
187
|
+
The R3 and R9 findings are the same, because the configuration sets `facts.stateMachines`, `facts.transitionTriggers` and `facts.scenarios`. So the generic adapter does not catch three mutations: `lib-r6-none-but-written`, `lib-r7-effect-unwitnessed` and `lib-r7-undeclared-store`. It catches the other 25, `lib-r3-transition-binding`, `lib-r6-link-sweep-missing` and `lib-r9-unknown-scenario` included. With the evidence ids kept, the clean toy fails with three violations: R1 for each of the two evidence ids, and R6 `fact_missing`.
|