stylegallery 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/README.md +22 -0
- package/consumer-reference/agent-native/v2/material-registry.json +1 -1
- package/log.md +46 -0
- package/package.json +12 -2
- package/patterns/media-fit/icon-frame.md +1 -1
- package/patterns/viewport-shell/holy-grail.md +7 -0
- package/quality/claim-records/samples.md +1453 -9
- package/quality/claims.md +14 -7
- package/scripts/compiler/cli.mjs +20 -0
- package/scripts/conservator-exhibits.mjs +768 -0
- package/scripts/conservator.mjs +326 -0
- package/scripts/markdown-structure.mjs +45 -0
- package/scripts/pattern-data.mjs +325 -0
- package/scripts/registrar.mjs +323 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// The Registrar keeps the gallery's claim register. Markdown claim records under
|
|
4
|
+
// quality/claim-records/ are the source of truth; the Registrar checks the machine-readable fields of
|
|
5
|
+
// each "## Claim Record" section against the template in quality/claims.md, the evidence-family
|
|
6
|
+
// glossary, the referenced files, the other records, and the Conservator's exhibit list. It cannot
|
|
7
|
+
// prove the semantics of a claim, only that its record is complete, well-formed, internally
|
|
8
|
+
// consistent, and points at evidence that exists.
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { exhibitSlugs } from "./conservator-exhibits.mjs";
|
|
13
|
+
import { structuralMarkdown } from "./markdown-structure.mjs";
|
|
14
|
+
|
|
15
|
+
const galleryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
const RECORD_DIR = "quality/claim-records";
|
|
17
|
+
const TEMPLATE_DOC = "quality/claims.md";
|
|
18
|
+
const FAMILY_DOC = "quality/evidence/families.md";
|
|
19
|
+
const RECORD_INDEX = `${RECORD_DIR}/index.md`;
|
|
20
|
+
const VERDICTS = new Set(["auto", "human", "mixed"]);
|
|
21
|
+
const DECISIONS = new Set(["pass", "review_required", "blocked"]);
|
|
22
|
+
const NA = "not applicable";
|
|
23
|
+
|
|
24
|
+
// Every field of the claims.md template, in template order. The template-sync rule fails when the
|
|
25
|
+
// template and this list drift apart.
|
|
26
|
+
export const RECORD_FIELDS = Object.freeze([
|
|
27
|
+
"Principle", "Claim", "Context", "Use case", "Warrant", "Evidence family", "Verification protocol",
|
|
28
|
+
"Implementation handoff", "Consumer reference", "Consumer reference reason", "Boundary or limitation",
|
|
29
|
+
"Debt or escalation", "Decision", "Evidence refs", "Verified by", "Reviewed at", "Review by",
|
|
30
|
+
"Source revision", "Conflicts with",
|
|
31
|
+
]);
|
|
32
|
+
// Fields that must state something; the rest may say "not applicable".
|
|
33
|
+
const STATEMENT_FIELDS = ["Principle", "Claim", "Context", "Warrant", "Evidence family", "Verification protocol", "Boundary or limitation", "Debt or escalation"];
|
|
34
|
+
// Human-outcome terms from quality/claims.md that automation alone cannot certify.
|
|
35
|
+
const HUMAN_OUTCOME_TERMS = ["good", "better", "best", "harmonious", "harmony", "usable", "usability", "accessible", "readable", "credible", "clear"];
|
|
36
|
+
|
|
37
|
+
export const REGISTRAR_RULES = Object.freeze([
|
|
38
|
+
"records-present", "record-title-format", "record-number-unique", "record-title-unique",
|
|
39
|
+
"template-sync", "field-present", "field-not-repeated", "statement-not-empty",
|
|
40
|
+
"decision-value", "verified-by-value", "reviewed-at-date", "reviewed-at-not-future",
|
|
41
|
+
"review-by-value", "review-by-after-reviewed-at", "review-by-overdue",
|
|
42
|
+
"source-revision-format", "external-source-review-by",
|
|
43
|
+
"conflicts-with-format", "conflicts-with-resolves", "conflicts-with-reciprocal",
|
|
44
|
+
"evidence-refs-present", "evidence-refs-backticked", "evidence-ref-kind", "evidence-ref-exists", "conservator-ref-resolves",
|
|
45
|
+
"evidence-family-known", "consumer-reference-format", "consumer-reference-reason",
|
|
46
|
+
"auto-pass-only", "auto-executable-refs", "auto-validator-family", "auto-no-human-outcome-terms",
|
|
47
|
+
"human-review-artifact", "record-file-indexed", "conservator-exhibit-recorded",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51
|
+
const isNa = (value) => value?.toLowerCase() === NA;
|
|
52
|
+
export const validDate = (value) => /^20\d\d-\d{2}-\d{2}$/.test(value)
|
|
53
|
+
&& Number.isFinite(Date.parse(`${value}T00:00:00Z`))
|
|
54
|
+
&& new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
|
|
55
|
+
const ticked = (value) => [...(value ?? "").matchAll(/`([^`]+)`/g)].map((match) => match[1].trim());
|
|
56
|
+
|
|
57
|
+
function listRecordFiles(root) {
|
|
58
|
+
const dir = path.join(root, RECORD_DIR);
|
|
59
|
+
if (!fs.existsSync(dir)) return [];
|
|
60
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
61
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
|
62
|
+
.map((entry) => `${RECORD_DIR}/${entry.name}`)
|
|
63
|
+
.sort();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function splitRecords(content) {
|
|
67
|
+
const records = [];
|
|
68
|
+
let current = null;
|
|
69
|
+
for (const line of content.split("\n")) {
|
|
70
|
+
const heading = line.match(/^## (Claim Record\b.*?)\s*$/);
|
|
71
|
+
if (heading) {
|
|
72
|
+
if (current) records.push(current);
|
|
73
|
+
current = { title: heading[1], body: [] };
|
|
74
|
+
} else if (line.startsWith("## ")) {
|
|
75
|
+
if (current) records.push(current);
|
|
76
|
+
current = null;
|
|
77
|
+
} else if (current) current.body.push(line);
|
|
78
|
+
}
|
|
79
|
+
if (current) records.push(current);
|
|
80
|
+
return records.map((record) => ({ title: record.title, body: record.body.join("\n") }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fieldValues(body, key) {
|
|
84
|
+
return [...body.matchAll(new RegExp(`^${escape(key)}:[ \\t]*(.*)$`, "gm"))].map((match) => match[1].trim());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function templateFields(root) {
|
|
88
|
+
const file = path.join(root, TEMPLATE_DOC);
|
|
89
|
+
if (!fs.existsSync(file)) return null;
|
|
90
|
+
const section = fs.readFileSync(file, "utf8").split(/^## Claim Record Template\s*$/m)[1];
|
|
91
|
+
const block = section?.match(/^```txt\n([\s\S]*?)^```/m)?.[1];
|
|
92
|
+
return block ? [...block.matchAll(/^([A-Z][^:\n]*):/gm)].map((match) => match[1]) : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function evidenceFamilies(root) {
|
|
96
|
+
const file = path.join(root, FAMILY_DOC);
|
|
97
|
+
if (!fs.existsSync(file)) return new Set();
|
|
98
|
+
return new Set([...structuralMarkdown(fs.readFileSync(file, "utf8")).matchAll(/^## `([a-z-]+)`\s*$/gm)].map((match) => match[1]));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function classifyRef(ref) {
|
|
102
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(ref)) return "id";
|
|
103
|
+
if (ref.includes("/")) return "path";
|
|
104
|
+
if (/^[a-z][a-z0-9-]*(?:\.[a-z0-9-]+)+$/.test(ref)) return "id";
|
|
105
|
+
return "invalid";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolvesInside(root, ref) {
|
|
109
|
+
if (/[\\]/.test(ref) || ref.startsWith("/")) return null;
|
|
110
|
+
const target = path.resolve(root, ref.split(/[?#]/)[0]);
|
|
111
|
+
return target.startsWith(`${path.resolve(root)}${path.sep}`) ? target : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const isFile = (target) => Boolean(target) && fs.existsSync(target) && fs.statSync(target).isFile();
|
|
115
|
+
|
|
116
|
+
// An installed package ships only its declared files; a repository checkout ships everything. A ref
|
|
117
|
+
// to a repository-only file is a warning in an installed package and a failure in a checkout.
|
|
118
|
+
function packagedFiles(root) {
|
|
119
|
+
if (fs.existsSync(path.join(root, ".git"))) return null;
|
|
120
|
+
try {
|
|
121
|
+
const files = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).files;
|
|
122
|
+
return Array.isArray(files) ? new Set(files) : null;
|
|
123
|
+
} catch { return null; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function recordAnchor(file, number) {
|
|
127
|
+
return `${file}#claim-record-${number}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function runRegistrar({ root = galleryRoot, now = new Date(), exhibitList = exhibitSlugs } = {}) {
|
|
131
|
+
const failures = [];
|
|
132
|
+
const warnings = [];
|
|
133
|
+
const fail = (rule, where, message) => failures.push(`${where}: ${message} [${rule}]`);
|
|
134
|
+
const warn = (rule, where, message) => warnings.push(`${where}: ${message} [${rule}]`);
|
|
135
|
+
const today = now.toISOString().slice(0, 10);
|
|
136
|
+
const files = listRecordFiles(root);
|
|
137
|
+
const families = evidenceFamilies(root);
|
|
138
|
+
const packaged = packagedFiles(root);
|
|
139
|
+
const exhibits = new Set(exhibitList);
|
|
140
|
+
const register = [];
|
|
141
|
+
|
|
142
|
+
const template = templateFields(root);
|
|
143
|
+
if (!template) fail("template-sync", TEMPLATE_DOC, "has no ```txt block under ## Claim Record Template");
|
|
144
|
+
else if (template.join("|") !== RECORD_FIELDS.join("|")) {
|
|
145
|
+
fail("template-sync", TEMPLATE_DOC, `template fields [${template.join(", ")}] differ from the Registrar's [${RECORD_FIELDS.join(", ")}]`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const indexText = fs.existsSync(path.join(root, RECORD_INDEX)) ? structuralMarkdown(fs.readFileSync(path.join(root, RECORD_INDEX), "utf8")) : "";
|
|
149
|
+
const titles = new Map();
|
|
150
|
+
|
|
151
|
+
for (const relative of files) {
|
|
152
|
+
const records = splitRecords(structuralMarkdown(fs.readFileSync(path.join(root, relative), "utf8")));
|
|
153
|
+
if (records.length > 0 && !new RegExp(`\\]\\(${escape(path.posix.basename(relative))}\\)`).test(indexText)) {
|
|
154
|
+
fail("record-file-indexed", relative, `holds claim records but is not linked from ${RECORD_INDEX}`);
|
|
155
|
+
}
|
|
156
|
+
const numbers = new Set();
|
|
157
|
+
for (const record of records) {
|
|
158
|
+
const where = `${relative}: ${record.title}`;
|
|
159
|
+
const heading = record.title.match(/^Claim Record (\d+) - (\S.*)$/);
|
|
160
|
+
if (!heading) fail("record-title-format", where, 'title must read "Claim Record <number> - <title>"');
|
|
161
|
+
const number = heading ? Number(heading[1]) : null;
|
|
162
|
+
if (number !== null) {
|
|
163
|
+
if (numbers.has(number)) fail("record-number-unique", where, `record number ${number} is used twice in ${relative}`);
|
|
164
|
+
numbers.add(number);
|
|
165
|
+
const key = heading[2].toLowerCase();
|
|
166
|
+
if (titles.has(key)) fail("record-title-unique", where, `title duplicates ${titles.get(key)}`);
|
|
167
|
+
else titles.set(key, where);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const values = {};
|
|
171
|
+
for (const key of RECORD_FIELDS) {
|
|
172
|
+
const found = fieldValues(record.body, key);
|
|
173
|
+
if (found.length > 1) fail("field-not-repeated", where, `${key} appears ${found.length} times`);
|
|
174
|
+
values[key] = found[0] ?? null;
|
|
175
|
+
if (values[key] === null) fail("field-present", where, `${key} field is missing`);
|
|
176
|
+
else if (values[key] === "") fail("field-present", where, `${key} must be a value or "${NA}"`);
|
|
177
|
+
}
|
|
178
|
+
for (const key of STATEMENT_FIELDS) {
|
|
179
|
+
if (values[key] && isNa(values[key])) fail("statement-not-empty", where, `${key.toLowerCase()} must be a non-empty statement`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const decision = values.Decision?.match(/^([a-z_]+)\b/)?.[1] ?? null;
|
|
183
|
+
if (values.Decision !== null && (!decision || !DECISIONS.has(decision))) fail("decision-value", where, "decision must begin with pass, review_required, or blocked");
|
|
184
|
+
const verifiedBy = values["Verified by"];
|
|
185
|
+
if (verifiedBy !== null && !VERDICTS.has(verifiedBy)) fail("verified-by-value", where, "verified by must be auto, human, or mixed");
|
|
186
|
+
|
|
187
|
+
const reviewedAt = values["Reviewed at"];
|
|
188
|
+
const reviewedOk = Boolean(reviewedAt) && validDate(reviewedAt);
|
|
189
|
+
if (reviewedAt !== null && !reviewedOk) fail("reviewed-at-date", where, "reviewed at must be a real YYYY-MM-DD date");
|
|
190
|
+
if (reviewedOk && reviewedAt > today) fail("reviewed-at-not-future", where, `reviewed at ${reviewedAt} is after today ${today}`);
|
|
191
|
+
|
|
192
|
+
const reviewBy = values["Review by"];
|
|
193
|
+
const reviewByDate = reviewBy && !isNa(reviewBy) && validDate(reviewBy) ? reviewBy : null;
|
|
194
|
+
if (reviewBy && !isNa(reviewBy) && !reviewByDate) fail("review-by-value", where, `review by must be a real YYYY-MM-DD date or "${NA}"`);
|
|
195
|
+
if (reviewByDate && reviewedOk && reviewByDate < reviewedAt) fail("review-by-after-reviewed-at", where, `review by ${reviewByDate} is before reviewed at ${reviewedAt}`);
|
|
196
|
+
const overdue = Boolean(reviewByDate && reviewByDate < today);
|
|
197
|
+
if (overdue) warn("review-by-overdue", where, `review by ${reviewByDate} has passed; re-review the claim`);
|
|
198
|
+
|
|
199
|
+
const revision = values["Source revision"];
|
|
200
|
+
if (revision && !isNa(revision) && !/^[0-9a-f]{7,40}$/.test(revision) && !/^https:\/\/\S+$/.test(revision)) {
|
|
201
|
+
fail("source-revision-format", where, `source revision must be a 7-40 character lowercase commit SHA, an https URL, or "${NA}"`);
|
|
202
|
+
}
|
|
203
|
+
if (revision && !isNa(revision) && !reviewByDate) fail("external-source-review-by", where, "a claim bound to an external source revision needs a review by date");
|
|
204
|
+
|
|
205
|
+
const conflicts = values["Conflicts with"];
|
|
206
|
+
const conflictRefs = conflicts && !isNa(conflicts) ? ticked(conflicts) : [];
|
|
207
|
+
if (conflicts && !isNa(conflicts) && conflictRefs.length === 0) fail("conflicts-with-format", where, `conflicts with must be backticked record refs or "${NA}"`);
|
|
208
|
+
for (const ref of conflictRefs) {
|
|
209
|
+
if (!/^quality\/claim-records\/[a-z0-9-]+\.md#claim-record-\d+$/.test(ref)) fail("conflicts-with-format", where, `conflict ref ${ref} must read quality/claim-records/<file>.md#claim-record-<number>`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const evidence = values["Evidence refs"];
|
|
213
|
+
let evidenceRefs = [];
|
|
214
|
+
if (evidence !== null && (!evidence || isNa(evidence))) fail("evidence-refs-present", where, "evidence refs must name at least one backticked ref");
|
|
215
|
+
else if (evidence) {
|
|
216
|
+
evidenceRefs = ticked(evidence);
|
|
217
|
+
if (evidenceRefs.length === 0) fail("evidence-refs-backticked", where, "evidence refs must be wrapped in backticks");
|
|
218
|
+
for (const ref of evidenceRefs) {
|
|
219
|
+
const kind = classifyRef(ref);
|
|
220
|
+
if (kind === "invalid") fail("evidence-ref-kind", where, `evidence ref ${ref} is neither a repository path nor a StableRef id`);
|
|
221
|
+
else if (kind === "path" && !isFile(resolvesInside(root, ref))) {
|
|
222
|
+
const inside = resolvesInside(root, ref);
|
|
223
|
+
if (inside && packaged && !packaged.has(ref.split(/[?#]/)[0])) warn("evidence-ref-exists", where, `evidence ref ${ref} is repository-only and not shipped in this package`);
|
|
224
|
+
else fail("evidence-ref-exists", where, `evidence ref ${ref} does not resolve to an existing repository file`);
|
|
225
|
+
}
|
|
226
|
+
const exhibit = ref.match(/^sg:\/\/conservator\/(.+)$/)?.[1];
|
|
227
|
+
if (exhibit !== undefined && !exhibits.has(exhibit)) fail("conservator-ref-resolves", where, `evidence ref ${ref} names no Conservator exhibit`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const familyValues = values["Evidence family"] && !isNa(values["Evidence family"]) ? ticked(values["Evidence family"]) : [];
|
|
232
|
+
if (values["Evidence family"] && !isNa(values["Evidence family"]) && familyValues.length === 0) fail("evidence-family-known", where, "evidence family must list backticked family names");
|
|
233
|
+
for (const family of familyValues) {
|
|
234
|
+
if (!families.has(family)) fail("evidence-family-known", where, `evidence family ${family} is not defined in ${FAMILY_DOC}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const consumer = values["Consumer reference"];
|
|
238
|
+
if (consumer && consumer !== "not_applicable") {
|
|
239
|
+
const target = ticked(consumer)[0] ?? consumer;
|
|
240
|
+
if (!/^[a-z0-9][a-z0-9._/-]*\.json$/.test(target) || target.includes("..") || !isFile(resolvesInside(root, target))) {
|
|
241
|
+
fail("consumer-reference-format", where, "consumer reference must be not_applicable or one existing repository-relative JSON record");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const reason = values["Consumer reference reason"];
|
|
245
|
+
if (consumer === "not_applicable" && reason !== null && !/^[A-Z].*\S[.!?]$/.test(reason)) {
|
|
246
|
+
fail("consumer-reference-reason", where, "a not_applicable consumer reference needs a one-sentence reason");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (verifiedBy === "auto") {
|
|
250
|
+
if (decision && decision !== "pass") fail("auto-pass-only", where, `auto verification is not allowed for a ${decision} decision`);
|
|
251
|
+
for (const ref of evidenceRefs) {
|
|
252
|
+
const executable = classifyRef(ref) === "id" || ref.startsWith("scripts/") || ref.startsWith("tests/");
|
|
253
|
+
if (!executable) fail("auto-executable-refs", where, `auto verification requires executable evidence refs (scripts/, tests/, or a StableRef id), not ${ref}`);
|
|
254
|
+
}
|
|
255
|
+
if (values["Evidence family"] && !familyValues.includes("validator")) fail("auto-validator-family", where, "auto verification requires the validator evidence family");
|
|
256
|
+
const term = HUMAN_OUTCOME_TERMS.find((word) => new RegExp(`\\b${word}\\b`, "i").test(values.Claim ?? ""));
|
|
257
|
+
if (term) fail("auto-no-human-outcome-terms", where, `auto verification cannot certify the human-outcome term "${term}" in the claim`);
|
|
258
|
+
} else if (verifiedBy === "human" || verifiedBy === "mixed") {
|
|
259
|
+
if (!evidenceRefs.some((ref) => ref.startsWith("quality/evidence/") || ref.startsWith("quality/gates/"))) {
|
|
260
|
+
fail("human-review-artifact", where, `${verifiedBy} verification requires a human-review evidence ref under quality/evidence/ or quality/gates/`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
register.push({
|
|
265
|
+
anchor: number === null ? null : recordAnchor(relative, number), file: relative, title: record.title,
|
|
266
|
+
decision, verified_by: verifiedBy, reviewed_at: reviewedAt, review_by: reviewBy, overdue, conflicts: conflictRefs,
|
|
267
|
+
exhibits: evidenceRefs.map((ref) => ref.match(/^sg:\/\/conservator\/(.+)$/)?.[1]).filter(Boolean),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const anchors = new Map(register.filter(({ anchor }) => anchor).map((entry) => [entry.anchor, entry]));
|
|
273
|
+
for (const entry of register) {
|
|
274
|
+
const where = `${entry.file}: ${entry.title}`;
|
|
275
|
+
for (const ref of entry.conflicts) {
|
|
276
|
+
const other = anchors.get(ref);
|
|
277
|
+
if (!other || other === entry) fail("conflicts-with-resolves", where, `conflict ref ${ref} names no other claim record`);
|
|
278
|
+
else if (!other.conflicts.includes(entry.anchor)) fail("conflicts-with-reciprocal", where, `conflict with ${ref} is not recorded on that record`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const slug of exhibitList) {
|
|
282
|
+
const owners = register.filter((entry) => entry.exhibits.includes(slug));
|
|
283
|
+
if (owners.length !== 1) fail("conservator-exhibit-recorded", `sg://conservator/${slug}`, `Conservator exhibit needs exactly one claim record citing it, found ${owners.length}`);
|
|
284
|
+
}
|
|
285
|
+
if (register.length === 0) fail("records-present", RECORD_DIR, "no claim records found");
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
ok: failures.length === 0, files: files.length, records: register.length, rules: REGISTRAR_RULES.length,
|
|
289
|
+
failures: [...new Set(failures)], warnings: [...new Set(warnings)], register,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Query views of the register for agents and reviewers (R6): records past their review date, and
|
|
294
|
+
// records whose verdict is not fully automated.
|
|
295
|
+
export function registrarView(result, { stale = false, unverified = false } = {}) {
|
|
296
|
+
let register = result.register;
|
|
297
|
+
if (stale) register = register.filter((entry) => entry.overdue);
|
|
298
|
+
if (unverified) register = register.filter((entry) => entry.verified_by !== "auto");
|
|
299
|
+
return { ...result, register };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function parseRegistrarArgs(argv) {
|
|
303
|
+
const options = { stale: false, unverified: false, root: undefined };
|
|
304
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
305
|
+
const flag = argv[index];
|
|
306
|
+
if (flag === "--stale" || flag === "--unverified") options[flag.slice(2)] = true;
|
|
307
|
+
else if (flag === "--root" && argv[index + 1] && !argv[index + 1].startsWith("--")) options.root = path.resolve(argv[++index]);
|
|
308
|
+
else if (flag === "--json") continue;
|
|
309
|
+
else throw Object.assign(new Error(`Unknown or incomplete registrar argument: ${flag}`), { code: "argument_invalid" });
|
|
310
|
+
}
|
|
311
|
+
return options;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
315
|
+
if (isMain) {
|
|
316
|
+
const argv = process.argv.slice(2);
|
|
317
|
+
const options = parseRegistrarArgs(argv);
|
|
318
|
+
const result = registrarView(runRegistrar({ root: options.root }), options);
|
|
319
|
+
if (argv.includes("--json")) console.log(JSON.stringify(result, null, 2));
|
|
320
|
+
else if (result.ok) console.log(`ok: ${result.records} claim records in ${result.files} files pass ${result.rules} Registrar rules${result.warnings.length ? `\n${result.warnings.join("\n")}` : ""}`);
|
|
321
|
+
else console.error(result.failures.join("\n"));
|
|
322
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
323
|
+
}
|