@githonllc/avouch 0.0.0-stage → 0.1.1
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 +161 -2
- package/SPEC.md +337 -0
- package/dist/cli/avouch.d.ts +2 -0
- package/dist/cli/avouch.js +51 -0
- package/dist/cli/check.d.ts +8 -0
- package/dist/cli/check.js +63 -0
- package/dist/cli/query.d.ts +2 -0
- package/dist/cli/query.js +150 -0
- package/dist/src/checker.d.ts +3 -0
- package/dist/src/checker.js +1001 -0
- package/dist/src/contract.d.ts +135 -0
- package/dist/src/contract.js +3 -0
- package/dist/src/evaluate.d.ts +66 -0
- package/dist/src/evaluate.js +478 -0
- package/dist/src/expr.d.ts +190 -0
- package/dist/src/expr.js +426 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +8 -0
- package/dist/src/markdown.d.ts +15 -0
- package/dist/src/markdown.js +476 -0
- package/dist/src/patch.d.ts +6 -0
- package/dist/src/patch.js +37 -0
- package/dist/src/query.d.ts +304 -0
- package/dist/src/query.js +681 -0
- package/dist/src/reconcile.d.ts +53 -0
- package/dist/src/reconcile.js +78 -0
- package/dist/src/stage1-check.d.ts +29 -0
- package/dist/src/stage1-check.js +305 -0
- package/dist/src/unknown.d.ts +10 -0
- package/dist/src/unknown.js +172 -0
- package/docs/adapter-guide.md +155 -0
- package/ontology.schema.json +1690 -0
- package/package.json +53 -4
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { parse } from "yaml";
|
|
5
|
+
import { isObj } from "../src/expr.js";
|
|
6
|
+
import { QUERY_LIST_KINDS, queryAction, queryCites, queryDisposition, queryList, queryObject, queryReads, queryWrites, } from "../src/query.js";
|
|
7
|
+
import { runCheck } from "./check.js";
|
|
8
|
+
export const QUERY_USAGE = "usage: avouch query <list|action|object|result|writes|reads|cites> <arg> <ontology.yaml> [--json] [--adapter <module|config.json>]";
|
|
9
|
+
const VERBS = ["list", "action", "object", "result", "writes", "reads", "cites"];
|
|
10
|
+
// Read-only git commands, run from the directory of the ontology file. null when any of them fails.
|
|
11
|
+
function gitStamp(file) {
|
|
12
|
+
const git = (...args) => {
|
|
13
|
+
const r = spawnSync("git", ["--no-optional-locks", ...args], { cwd: dirname(file), encoding: "utf8" });
|
|
14
|
+
return r.error !== undefined || r.status !== 0 ? null : r.stdout;
|
|
15
|
+
};
|
|
16
|
+
const commit = git("rev-parse", "HEAD");
|
|
17
|
+
const top = git("rev-parse", "--show-toplevel");
|
|
18
|
+
const status = git("status", "--porcelain", "--", file);
|
|
19
|
+
if (commit === null || top === null || status === null)
|
|
20
|
+
return null;
|
|
21
|
+
// status prints nothing for an ignored file; a file that is not tracked is in no commit, so it is dirty.
|
|
22
|
+
const tracked = git("ls-files", "--error-unmatch", "--", file);
|
|
23
|
+
return { commit: commit.trim(), dirty: status.trim() !== "" || tracked === null, path: relative(top.trim(), file).split(sep).join("/") };
|
|
24
|
+
}
|
|
25
|
+
function run(verb, arg, ont) {
|
|
26
|
+
switch (verb) {
|
|
27
|
+
case "list": {
|
|
28
|
+
const r = queryList(ont, arg);
|
|
29
|
+
return { result: r, empty: r === null || r.items.length === 0 };
|
|
30
|
+
}
|
|
31
|
+
case "cites": {
|
|
32
|
+
const r = queryCites(ont, arg);
|
|
33
|
+
return { result: r, empty: r.claims.length === 0 };
|
|
34
|
+
}
|
|
35
|
+
default: {
|
|
36
|
+
const f = { action: queryAction, object: queryObject, result: queryDisposition, writes: queryWrites, reads: queryReads }[verb];
|
|
37
|
+
const r = f(ont, arg);
|
|
38
|
+
return { result: r, empty: r === null };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------------------------------------- text
|
|
43
|
+
const isScalar = (x) => x === null || typeof x !== "object";
|
|
44
|
+
const isCite = (x) => isObj(x) && Object.keys(x).length === 2 && typeof x.doc === "string" && (typeof x.quote === "string" || x.quote === null);
|
|
45
|
+
// A scalar on one line: a line break becomes the two characters \n (or \r).
|
|
46
|
+
const esc = (x) => String(x).replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
47
|
+
// The one-line form of a value, or undefined when it needs a block.
|
|
48
|
+
function inline(v) {
|
|
49
|
+
if (v === null || v === undefined)
|
|
50
|
+
return "-";
|
|
51
|
+
if (typeof v !== "object")
|
|
52
|
+
return esc(v);
|
|
53
|
+
if (isCite(v))
|
|
54
|
+
return v.quote === null ? `[${esc(v.doc)}]` : `[${esc(v.doc)}] ${esc(v.quote)}`;
|
|
55
|
+
if (Array.isArray(v)) {
|
|
56
|
+
if (v.length === 0)
|
|
57
|
+
return "-";
|
|
58
|
+
return v.every(isScalar) ? v.map((x) => (x === null ? "-" : esc(x))).join(", ") : undefined;
|
|
59
|
+
}
|
|
60
|
+
return Object.keys(v).length === 0 ? "-" : undefined;
|
|
61
|
+
}
|
|
62
|
+
function block(v, pad) {
|
|
63
|
+
if (Array.isArray(v))
|
|
64
|
+
return v.flatMap((el) => {
|
|
65
|
+
const s = inline(el);
|
|
66
|
+
if (s !== undefined)
|
|
67
|
+
return [`${pad}- ${s}`];
|
|
68
|
+
const sub = block(el, `${pad} `);
|
|
69
|
+
if (sub.length === 0)
|
|
70
|
+
return [`${pad}- -`];
|
|
71
|
+
return [`${pad}- ${sub[0].slice(pad.length + 2)}`, ...sub.slice(1)];
|
|
72
|
+
});
|
|
73
|
+
const o = v;
|
|
74
|
+
return Object.keys(o)
|
|
75
|
+
.filter((k) => !(k === "expr" && "exprText" in o))
|
|
76
|
+
.flatMap((k) => {
|
|
77
|
+
const s = inline(o[k]);
|
|
78
|
+
return s !== undefined ? [`${pad}${k}: ${s}`] : [`${pad}${k}:`, ...block(o[k], `${pad} `)];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function text(env) {
|
|
82
|
+
const { stamp } = env;
|
|
83
|
+
const c = stamp.check;
|
|
84
|
+
const lines = [
|
|
85
|
+
`ontology: ${stamp.git?.path ?? stamp.ontology}`,
|
|
86
|
+
stamp.git === null ? "commit: none (not in git)" : `commit: ${stamp.git.commit}${stamp.git.dirty ? " (dirty)" : ""}`,
|
|
87
|
+
c.status === "not_run" ? "check: not_run" : `check: ${c.status} (${c.schemaErrors} schema error(s), ${c.violations} violation(s), ${c.waived} waived)`,
|
|
88
|
+
"",
|
|
89
|
+
];
|
|
90
|
+
const s = inline(env.result);
|
|
91
|
+
return [...lines, ...(s !== undefined ? [s] : block(env.result, ""))].join("\n") + "\n";
|
|
92
|
+
}
|
|
93
|
+
// ---------------------------------------------------------------------------------------------------------- main
|
|
94
|
+
function parseArgs(args) {
|
|
95
|
+
const pos = [];
|
|
96
|
+
let json = false;
|
|
97
|
+
let adapter = null;
|
|
98
|
+
for (let i = 0; i < args.length; i++) {
|
|
99
|
+
const a = args[i];
|
|
100
|
+
if (a === "--json")
|
|
101
|
+
json = true;
|
|
102
|
+
else if (a === "--adapter") {
|
|
103
|
+
if (i + 1 >= args.length)
|
|
104
|
+
throw new Error("--adapter needs a module");
|
|
105
|
+
adapter = args[++i];
|
|
106
|
+
}
|
|
107
|
+
else if (a.startsWith("--"))
|
|
108
|
+
throw new Error(`unknown option ${a}`);
|
|
109
|
+
else
|
|
110
|
+
pos.push(a);
|
|
111
|
+
}
|
|
112
|
+
if (pos.length !== 3)
|
|
113
|
+
throw new Error(`expected 3 arguments, found ${pos.length}`);
|
|
114
|
+
const [verb, arg, file] = pos;
|
|
115
|
+
if (!VERBS.includes(verb))
|
|
116
|
+
throw new Error(`unknown verb ${verb}`);
|
|
117
|
+
if (verb === "list" && !QUERY_LIST_KINDS.includes(arg))
|
|
118
|
+
throw new Error(`unknown list kind ${arg}`);
|
|
119
|
+
if ((verb === "writes" || verb === "reads") && arg.split(".").length !== 2)
|
|
120
|
+
throw new Error(`${verb} takes <Object>.<property>`);
|
|
121
|
+
return { verb: verb, arg, file, json, adapter };
|
|
122
|
+
}
|
|
123
|
+
async function query(args) {
|
|
124
|
+
const { verb, arg, file, json, adapter } = parseArgs(args);
|
|
125
|
+
const real = realpathSync(resolve(file));
|
|
126
|
+
const ont = parse(readFileSync(real, "utf8"));
|
|
127
|
+
if (!isObj(ont))
|
|
128
|
+
throw new Error(`${file}: the root is not a mapping`);
|
|
129
|
+
let check = { status: "not_run" };
|
|
130
|
+
if (adapter !== null) {
|
|
131
|
+
const { schemaErrors, report } = await runCheck(file, adapter);
|
|
132
|
+
const pass = schemaErrors.length === 0 && report.violations.length === 0;
|
|
133
|
+
check = { status: pass ? "pass" : "fail", adapter, schemaErrors: schemaErrors.length, violations: report.violations.length, waived: report.waived.length };
|
|
134
|
+
}
|
|
135
|
+
const { result, empty } = run(verb, arg, ont);
|
|
136
|
+
const env = { avouchQuery: 1, stamp: { ontology: real, git: gitStamp(real), check }, verb, arg, result };
|
|
137
|
+
process.stdout.write(json ? `${JSON.stringify(env, null, 2)}\n` : text(env));
|
|
138
|
+
return empty ? 1 : 0;
|
|
139
|
+
}
|
|
140
|
+
// Exit 0: a result; 1: no result (null or an empty list); 2: a usage, file, parse or adapter error (one stderr line).
|
|
141
|
+
export async function runQuery(args) {
|
|
142
|
+
try {
|
|
143
|
+
return await query(args);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const msg = (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, " ");
|
|
147
|
+
console.error(`${msg}; ${QUERY_USAGE}`);
|
|
148
|
+
return 2;
|
|
149
|
+
}
|
|
150
|
+
}
|