@tiphys/kernel 0.0.0 → 0.2.0
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/AGENTS.md +663 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +299 -0
- package/checklists/clean-room.yaml +325 -0
- package/checklists/env-failure-diagnosis.yaml +68 -0
- package/checklists/flake-playbook.yaml +68 -0
- package/checklists/hazard-review.yaml +144 -0
- package/checklists/plan-review.yaml +103 -0
- package/dist/bin/tiphys.d.ts +2 -0
- package/dist/bin/tiphys.js +92 -0
- package/dist/src/adapters/load.d.ts +202 -0
- package/dist/src/adapters/load.js +440 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +66 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +1539 -0
- package/dist/src/checks.js +5025 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +68 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +343 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/cutover.d.ts +35 -0
- package/dist/src/commands/cutover.js +448 -0
- package/dist/src/commands/doctor.d.ts +264 -0
- package/dist/src/commands/doctor.js +1546 -0
- package/dist/src/commands/gates.d.ts +9 -0
- package/dist/src/commands/gates.js +360 -0
- package/dist/src/commands/init.d.ts +30 -0
- package/dist/src/commands/init.js +177 -0
- package/dist/src/commands/lock.d.ts +34 -0
- package/dist/src/commands/lock.js +340 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -0
- package/dist/src/commands/next.d.ts +130 -0
- package/dist/src/commands/next.js +597 -0
- package/dist/src/commands/plan.d.ts +20 -0
- package/dist/src/commands/plan.js +105 -0
- package/dist/src/commands/pool.d.ts +1 -0
- package/dist/src/commands/pool.js +139 -0
- package/dist/src/commands/resume.d.ts +1 -0
- package/dist/src/commands/resume.js +88 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +195 -0
- package/dist/src/commands/status.d.ts +21 -0
- package/dist/src/commands/status.js +122 -0
- package/dist/src/commands/sync.d.ts +47 -0
- package/dist/src/commands/sync.js +341 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +87 -0
- package/dist/src/commands/tuition.d.ts +21 -0
- package/dist/src/commands/tuition.js +218 -0
- package/dist/src/commands/validate.d.ts +78 -0
- package/dist/src/commands/validate.js +430 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/cutover.d.ts +584 -0
- package/dist/src/cutover.js +1444 -0
- package/dist/src/exclusion.d.ts +389 -0
- package/dist/src/exclusion.js +843 -0
- package/dist/src/exec/env.d.ts +278 -0
- package/dist/src/exec/env.js +334 -0
- package/dist/src/fleet.d.ts +223 -0
- package/dist/src/fleet.js +298 -0
- package/dist/src/gates/adapters/http-json.d.ts +5 -0
- package/dist/src/gates/adapters/http-json.js +283 -0
- package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
- package/dist/src/gates/adapters/migrations-command.js +373 -0
- package/dist/src/gates/citations.d.ts +408 -0
- package/dist/src/gates/citations.js +1169 -0
- package/dist/src/gates/coverage.d.ts +375 -0
- package/dist/src/gates/coverage.js +836 -0
- package/dist/src/gates/credentials.d.ts +233 -0
- package/dist/src/gates/credentials.js +752 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/gate-classes.d.ts +56 -0
- package/dist/src/gates/gate-classes.js +633 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -0
- package/dist/src/gates/merge-preconditions.d.ts +319 -0
- package/dist/src/gates/merge-preconditions.js +932 -0
- package/dist/src/gates/migrations.d.ts +1 -0
- package/dist/src/gates/migrations.js +36 -0
- package/dist/src/gates/pin.d.ts +114 -0
- package/dist/src/gates/pin.js +154 -0
- package/dist/src/gates/red-witness.d.ts +22 -0
- package/dist/src/gates/red-witness.js +482 -0
- package/dist/src/gates/release.d.ts +283 -0
- package/dist/src/gates/release.js +820 -0
- package/dist/src/gates/result.d.ts +116 -0
- package/dist/src/gates/result.js +91 -0
- package/dist/src/gates/run.d.ts +614 -0
- package/dist/src/gates/run.js +1614 -0
- package/dist/src/gates/schemas/citation-config.schema.json +59 -0
- package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
- package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
- package/dist/src/gates/schemas/gate-result.schema.json +160 -0
- package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
- package/dist/src/gates/schemas/release-record.schema.json +119 -0
- package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
- package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
- package/dist/src/gates/scope.d.ts +131 -0
- package/dist/src/gates/scope.js +1018 -0
- package/dist/src/gates/suite.d.ts +217 -0
- package/dist/src/gates/suite.js +968 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +84 -0
- package/dist/src/hooks.js +125 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.js +30 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +256 -0
- package/dist/src/lock.js +792 -0
- package/dist/src/model-resolution.d.ts +159 -0
- package/dist/src/model-resolution.js +307 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +34 -0
- package/dist/src/path-identity.js +48 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +326 -0
- package/dist/src/pool.js +988 -0
- package/dist/src/roles.d.ts +461 -0
- package/dist/src/roles.js +776 -0
- package/dist/src/spawn.d.ts +482 -0
- package/dist/src/spawn.js +1003 -0
- package/dist/src/status.d.ts +116 -0
- package/dist/src/status.js +148 -0
- package/dist/src/task.d.ts +504 -0
- package/dist/src/task.js +307 -0
- package/dist/src/teardown.d.ts +39 -0
- package/dist/src/teardown.js +422 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +263 -0
- package/dist/src/validate.js +742 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.js +38 -0
- package/dist/src/watcher.d.ts +275 -0
- package/dist/src/watcher.js +849 -0
- package/dist/src/witness/run.d.ts +299 -0
- package/dist/src/witness/run.js +1373 -0
- package/dist/src/witness/spec.d.ts +270 -0
- package/dist/src/witness/spec.js +475 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +526 -0
- package/gates.manifest.json +257 -0
- package/package.json +72 -3
- package/role-model-config.yaml +88 -0
- package/roles/README.md +128 -0
- package/roles/_shared-dispatch-contract.md +87 -0
- package/roles/adversarial-plan-reviewer.md +80 -0
- package/roles/clean-room-reviewer.md +140 -0
- package/roles/implementer.md +463 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +82 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +185 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/cutover-state.schema.json +64 -0
- package/schemas/decision-record.schema.json +88 -0
- package/schemas/executor-record.schema.json +36 -0
- package/schemas/final-report.schema.json +90 -0
- package/schemas/finding.schema.json +106 -0
- package/schemas/gate-registry.schema.json +260 -0
- package/schemas/mechanism-index.schema.json +94 -0
- package/schemas/model-resolution.schema.json +362 -0
- package/schemas/plan.schema.json +300 -0
- package/schemas/report.schema.json +579 -0
- package/schemas/role-brief.schema.json +105 -0
- package/schemas/role-model-config.schema.json +90 -0
- package/schemas/status-line.schema.json +40 -0
- package/schemas/tuition.schema.json +191 -0
- package/schemas/verdict.schema.json +295 -0
- package/schemas/work-history.schema.json +183 -0
- package/schemas/write-bypass.schema.json +69 -0
- package/templates/charter.example.yaml +54 -0
- package/templates/decision-record.example.yaml +27 -0
- package/templates/final-report.example.yaml +80 -0
- package/templates/plan.example.yaml +87 -0
- package/templates/report.example.yaml +236 -0
- package/templates/warnings.md +74 -0
- package/templates/work-history.example.yaml +185 -0
- package/tuition/README.md +76 -0
- package/tuition/T-001.yaml +48 -0
- package/tuition/T-002.yaml +51 -0
- package/tuition/T-003.yaml +100 -0
- package/tuition/T-004.yaml +52 -0
- package/tuition/T-005.yaml +72 -0
- package/tuition/T-006.yaml +81 -0
- package/tuition/T-007.yaml +56 -0
- package/tuition/T-008.yaml +111 -0
- package/tuition/T-009.yaml +50 -0
- package/tuition/T-015.yaml +36 -0
- package/tuition/T-016.yaml +36 -0
- package/tuition/T-017.yaml +46 -0
- package/tuition/T-018.yaml +84 -0
- package/tuition/T-021.yaml +40 -0
- package/tuition/T-022.yaml +36 -0
- package/tuition/mechanism-index.yaml +256 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE TUITION FEED AND THE MECHANISM INDEX (kernel plan M3, M3-P8; R-091).
|
|
3
|
+
*
|
|
4
|
+
* A tuition entry records one failure mode a delivery paid for. The MECHANISM
|
|
5
|
+
* INDEX is a PROJECTION of the `mechanisms[]` field of every entry in the
|
|
6
|
+
* feed, keyed by mechanism, and it is generated rather than authored. T-005 is
|
|
7
|
+
* why both halves exist and why they are one artifact rather than two: a rule
|
|
8
|
+
* M1-P3 paid for did not reach M1-P5, which reimplemented the same claim-file
|
|
9
|
+
* mechanism silently and produced the most severe defect found in M1, and a
|
|
10
|
+
* second hand-maintained copy of a rule is the state that produced it.
|
|
11
|
+
*
|
|
12
|
+
* THE TWO LAYERS ARE STRUCTURALLY DISTINCT, which is the plan's compaction
|
|
13
|
+
* model (step 2c). The index is the READ layer: dense, consulted at every
|
|
14
|
+
* dispatch under the `mechanism-lookup` obligation. The entries are the
|
|
15
|
+
* ARCHIVE layer: longer, read when a rule is disputed. `tuition index --check`
|
|
16
|
+
* is what keeps the first honest about the second.
|
|
17
|
+
*
|
|
18
|
+
* PATHS THIS MODULE READS ARE NOT ITS OWN (D-M3-27, the mechanism index's own
|
|
19
|
+
* row "Reading a path whose type is not established"). Every read goes through
|
|
20
|
+
* `readOperatorPath`, so a named pipe at an entry path is refused with the
|
|
21
|
+
* observed entry type instead of blocking the command forever.
|
|
22
|
+
*/
|
|
23
|
+
import { readdirSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { classifyContextDirectory, decodeDocument, formatDiagnostics, readOperatorPath, validateInstance, } from "./validate.js";
|
|
26
|
+
import { loadTypeSchema } from "./commands/validate.js";
|
|
27
|
+
/** The generated index, relative to a tuition directory. */
|
|
28
|
+
export const MECHANISM_INDEX_FILE = "mechanism-index.yaml";
|
|
29
|
+
/**
|
|
30
|
+
* THE KEY IS DERIVED FROM THE NAME, NEVER INVENTED: lowercase, every run of
|
|
31
|
+
* characters outside [a-z0-9] collapsed to one hyphen, ends trimmed. The same
|
|
32
|
+
* derivation `schemas/mechanism-index.schema.json` documents and M3-P6's
|
|
33
|
+
* registered test applies, stated once in code so the generator and the check
|
|
34
|
+
* cannot disagree about it.
|
|
35
|
+
*/
|
|
36
|
+
export function mechanismKey(name) {
|
|
37
|
+
return name
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
40
|
+
.replace(/^-+|-+$/g, "");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Every tuition ENTRY file in a directory, sorted by name.
|
|
44
|
+
*
|
|
45
|
+
* The generated index lives in the same directory and is EXCLUDED BY NAME: it
|
|
46
|
+
* is the projection's output, and a projection that reads its own output is a
|
|
47
|
+
* loop rather than a check. Anything that is not a `.yaml` file is ignored, so
|
|
48
|
+
* a README beside the feed is not an entry.
|
|
49
|
+
*/
|
|
50
|
+
export function listEntryFiles(directory) {
|
|
51
|
+
const problem = classifyContextDirectory(directory);
|
|
52
|
+
if (problem !== undefined) {
|
|
53
|
+
return { ok: false, reason: problem };
|
|
54
|
+
}
|
|
55
|
+
let names;
|
|
56
|
+
try {
|
|
57
|
+
names = readdirSync(directory);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
reason: `${directory} could not be listed: ${String(error)}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const paths = names
|
|
66
|
+
.filter((name) => name.endsWith(".yaml") && name !== MECHANISM_INDEX_FILE)
|
|
67
|
+
.sort()
|
|
68
|
+
.map((name) => join(directory, name));
|
|
69
|
+
return { ok: true, paths };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read, decode and schema-validate one tuition entry.
|
|
73
|
+
*
|
|
74
|
+
* THE RAW BYTES COME BACK WITH THE ENTRY, and that is not a convenience. An
|
|
75
|
+
* earlier version had `tuition add` call this and then read the same path a
|
|
76
|
+
* second time for the bytes to write. Two reads meant two independent
|
|
77
|
+
* refusals of a non-regular path, and a refusal that another refusal shadows
|
|
78
|
+
* cannot be witnessed: mutating either left the other rejecting the same
|
|
79
|
+
* input, which is exactly the shape T-018 records. One read, one
|
|
80
|
+
* classification, one guard.
|
|
81
|
+
*/
|
|
82
|
+
export function loadEntry(path) {
|
|
83
|
+
const read = readOperatorPath(path);
|
|
84
|
+
if (!read.ok) {
|
|
85
|
+
return { ok: false, reason: read.reason, diagnostics: [] };
|
|
86
|
+
}
|
|
87
|
+
const decoded = decodeDocument(read.body, path);
|
|
88
|
+
if (!decoded.ok) {
|
|
89
|
+
return { ok: false, reason: decoded.reason, diagnostics: [] };
|
|
90
|
+
}
|
|
91
|
+
const diagnostics = formatDiagnostics(validateInstance(loadTypeSchema("tuition"), decoded.value));
|
|
92
|
+
if (diagnostics.length > 0) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: `${path} is not a valid tuition entry`,
|
|
96
|
+
diagnostics,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, entry: decoded.value, body: read.body };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Project the mechanism index out of a set of entries, sorted by key.
|
|
103
|
+
*
|
|
104
|
+
* A KEY CLAIMED BY TWO ENTRIES IS AN ERROR NAMING BOTH, never a silent
|
|
105
|
+
* first-wins. Two entries stating the same mechanism differently is exactly
|
|
106
|
+
* the divergence this document exists to prevent, and the projection cannot
|
|
107
|
+
* decide which of the two rules the project actually learned. The resolution
|
|
108
|
+
* is an editorial one: merge the two entries' rules by hand into whichever one
|
|
109
|
+
* owns the mechanism.
|
|
110
|
+
*/
|
|
111
|
+
export function projectIndex(entries) {
|
|
112
|
+
const byKey = new Map();
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
for (const declaration of entry.mechanisms ?? []) {
|
|
115
|
+
const key = mechanismKey(declaration.mechanism);
|
|
116
|
+
if (key === "") {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
reason: `${entry.id} declares mechanism ${JSON.stringify(declaration.mechanism)}, whose derived key is empty`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const existing = byKey.get(key);
|
|
123
|
+
if (existing !== undefined) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
reason: `mechanism ${key} is declared by both ${existing.source} and ${entry.id}; one entry owns a mechanism`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const row = {
|
|
130
|
+
key,
|
|
131
|
+
name: declaration.mechanism,
|
|
132
|
+
rule: declaration.rule,
|
|
133
|
+
evidence: [...declaration.evidence],
|
|
134
|
+
source: entry.id,
|
|
135
|
+
};
|
|
136
|
+
if (declaration.siblings !== undefined) {
|
|
137
|
+
row.siblings = [...declaration.siblings];
|
|
138
|
+
}
|
|
139
|
+
const machine = declaration["machine-readable-form"];
|
|
140
|
+
if (machine !== undefined) {
|
|
141
|
+
row["machine-readable-form"] = { path: machine.path, key: machine.key };
|
|
142
|
+
}
|
|
143
|
+
byKey.set(key, row);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const rows = [...byKey.values()].sort((a, b) => (a.key < b.key ? -1 : 1));
|
|
147
|
+
return { ok: true, rows };
|
|
148
|
+
}
|
|
149
|
+
/* ------------------------------------------------------------------ */
|
|
150
|
+
/* Rendering */
|
|
151
|
+
/* ------------------------------------------------------------------ */
|
|
152
|
+
/**
|
|
153
|
+
* Emit a YAML scalar, quoting only when a plain one would not round trip.
|
|
154
|
+
*
|
|
155
|
+
* THE CONDITION IS THE POINT: this generator's output is re-read by
|
|
156
|
+
* `--check`, so a scalar that YAML would decode as something other than the
|
|
157
|
+
* string handed in makes the projection disagree with itself. The reserved
|
|
158
|
+
* leading indicators, an embedded `: ` or ` #`, and a trailing colon are the
|
|
159
|
+
* cases; everything else is emitted plain, which is what keeps the document
|
|
160
|
+
* readable.
|
|
161
|
+
*/
|
|
162
|
+
export function yamlScalar(value) {
|
|
163
|
+
const risky = value === "" ||
|
|
164
|
+
/^[-?:,[\]{}#&*!|>'"%@`]/.test(value) ||
|
|
165
|
+
value.includes(": ") ||
|
|
166
|
+
value.includes(" #") ||
|
|
167
|
+
value.endsWith(":") ||
|
|
168
|
+
value.includes("\n") ||
|
|
169
|
+
value.trim() !== value;
|
|
170
|
+
return risky ? JSON.stringify(value) : value;
|
|
171
|
+
}
|
|
172
|
+
/** Fold one paragraph to a width, at spaces, with a fixed indent. */
|
|
173
|
+
function foldedBlock(text, indent, width) {
|
|
174
|
+
const words = text.split(/\s+/).filter((word) => word !== "");
|
|
175
|
+
const lines = [];
|
|
176
|
+
let current = "";
|
|
177
|
+
for (const word of words) {
|
|
178
|
+
if (current === "") {
|
|
179
|
+
current = word;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (`${current} ${word}`.length + indent.length > width) {
|
|
183
|
+
lines.push(`${indent}${current}`);
|
|
184
|
+
current = word;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
current = `${current} ${word}`;
|
|
188
|
+
}
|
|
189
|
+
if (current !== "") {
|
|
190
|
+
lines.push(`${indent}${current}`);
|
|
191
|
+
}
|
|
192
|
+
return lines;
|
|
193
|
+
}
|
|
194
|
+
const HEADER = `# THE MECHANISM INDEX (kernel plan M3; T-005, D-M3-23).
|
|
195
|
+
#
|
|
196
|
+
# GENERATED BY \`tiphys tuition index\`. DO NOT EDIT THIS FILE. It is a
|
|
197
|
+
# PROJECTION of the \`mechanisms[]\` field of every entry in the tuition feed
|
|
198
|
+
# beside it, so a rule is recorded once, in the entry that paid for it, and
|
|
199
|
+
# read from here. \`tiphys tuition index --check\` compares this file against a
|
|
200
|
+
# fresh projection and exits nonzero on any drift, which is what stops the two
|
|
201
|
+
# from becoming two sources.
|
|
202
|
+
#
|
|
203
|
+
# READ THE ROW BEFORE YOU USE THE MECHANISM. Every rule here was paid for with
|
|
204
|
+
# a defect, a fix round, or an investigation, which is why \`evidence\` is a
|
|
205
|
+
# required field with at least one entry: a rule with no citation is not a
|
|
206
|
+
# rule.
|
|
207
|
+
#
|
|
208
|
+
# WHY THIS FILE EXISTS AT ALL. T-005 records a rule M1-P3 paid for in a
|
|
209
|
+
# multi-hour investigation that did not reach M1-P5, which reimplemented the
|
|
210
|
+
# same claim-file mechanism silently and produced the most severe defect found
|
|
211
|
+
# in that milestone. The implementer there had read the plan, the agent-rules
|
|
212
|
+
# file, the constraint list, the accumulated environment warnings and three
|
|
213
|
+
# work histories. None of them carried the rule, because a rule about a
|
|
214
|
+
# MECHANISM has no home in a set of documents organised by phase. This is that
|
|
215
|
+
# home, and the obligation to consult it is the \`mechanism-lookup\` clause in
|
|
216
|
+
# roles/implementer.md.
|
|
217
|
+
#
|
|
218
|
+
# \`key\` IS DERIVED FROM \`name\`, NOT INVENTED: lowercase, every run of
|
|
219
|
+
# characters outside [a-z0-9] collapsed to one hyphen, ends trimmed.
|
|
220
|
+
`;
|
|
221
|
+
/** Render the index document. The bytes are what `--check` compares. */
|
|
222
|
+
export function renderIndex(rows) {
|
|
223
|
+
const lines = [HEADER, "kind: mechanism-index", "version: 1", "", "mechanisms:"];
|
|
224
|
+
for (const row of rows) {
|
|
225
|
+
lines.push(` - key: ${row.key}`);
|
|
226
|
+
lines.push(` name: ${yamlScalar(row.name)}`);
|
|
227
|
+
lines.push(" rule: >-");
|
|
228
|
+
lines.push(...foldedBlock(row.rule, " ", 78));
|
|
229
|
+
if (row.siblings !== undefined) {
|
|
230
|
+
if (row.siblings.length === 0) {
|
|
231
|
+
lines.push(" siblings: []");
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
lines.push(" siblings:");
|
|
235
|
+
for (const sibling of row.siblings) {
|
|
236
|
+
lines.push(` - ${yamlScalar(sibling)}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const machine = row["machine-readable-form"];
|
|
241
|
+
if (machine !== undefined) {
|
|
242
|
+
lines.push(" machine-readable-form:");
|
|
243
|
+
lines.push(` path: ${yamlScalar(machine.path)}`);
|
|
244
|
+
lines.push(` key: ${yamlScalar(machine.key)}`);
|
|
245
|
+
}
|
|
246
|
+
lines.push(" evidence:");
|
|
247
|
+
for (const item of row.evidence) {
|
|
248
|
+
lines.push(` - ${yamlScalar(item)}`);
|
|
249
|
+
}
|
|
250
|
+
lines.push("");
|
|
251
|
+
}
|
|
252
|
+
/* One trailing newline, never two: the last row already pushed a blank. */
|
|
253
|
+
return `${lines.join("\n").replace(/\n+$/, "")}\n`;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Compare a committed index document against a fresh projection.
|
|
257
|
+
*
|
|
258
|
+
* NAMES THE MECHANISM AND THE ENTRY IT CAME FROM, which is the criterion's
|
|
259
|
+
* letter (4): a reader of a red `--check` must be able to go straight to the
|
|
260
|
+
* file that changed. Both directions are reported, because a row DELETED from
|
|
261
|
+
* the feed and a row ADDED to it are different faults with the same symptom.
|
|
262
|
+
*
|
|
263
|
+
* The field comparison is over the DECODED values rather than the bytes, so a
|
|
264
|
+
* rewrap of a folded scalar is not reported as a rule change; a byte
|
|
265
|
+
* comparison is done by the caller afterwards and reported as formatting,
|
|
266
|
+
* which keeps "the rule changed" and "the file was hand-edited" apart.
|
|
267
|
+
*/
|
|
268
|
+
export function driftLines(committed, rows) {
|
|
269
|
+
const problems = [];
|
|
270
|
+
const document = committed;
|
|
271
|
+
const committedRows = Array.isArray(document?.mechanisms)
|
|
272
|
+
? document.mechanisms
|
|
273
|
+
: [];
|
|
274
|
+
const committedByKey = new Map();
|
|
275
|
+
for (const row of committedRows) {
|
|
276
|
+
if (typeof row?.key === "string") {
|
|
277
|
+
committedByKey.set(row.key, row);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const row of rows) {
|
|
281
|
+
const found = committedByKey.get(row.key);
|
|
282
|
+
if (found === undefined) {
|
|
283
|
+
problems.push(`mechanism ${row.key} is declared by tuition entry ${row.source} and is missing from the committed index`);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const expected = JSON.stringify({
|
|
287
|
+
name: row.name,
|
|
288
|
+
rule: row.rule,
|
|
289
|
+
siblings: row.siblings ?? null,
|
|
290
|
+
machine: row["machine-readable-form"] ?? null,
|
|
291
|
+
evidence: row.evidence,
|
|
292
|
+
});
|
|
293
|
+
const actual = JSON.stringify({
|
|
294
|
+
name: found.name,
|
|
295
|
+
rule: typeof found.rule === "string" ? found.rule.trim() : found.rule,
|
|
296
|
+
siblings: found.siblings ?? null,
|
|
297
|
+
machine: found["machine-readable-form"] ?? null,
|
|
298
|
+
evidence: found.evidence,
|
|
299
|
+
});
|
|
300
|
+
if (expected !== actual) {
|
|
301
|
+
problems.push(`mechanism ${row.key} differs from the projection of tuition entry ${row.source}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const projected = new Set(rows.map((row) => row.key));
|
|
305
|
+
for (const key of [...committedByKey.keys()].sort()) {
|
|
306
|
+
if (!projected.has(key)) {
|
|
307
|
+
problems.push(`mechanism ${key} is in the committed index and no tuition entry declares it`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return problems;
|
|
311
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE KERNEL'S SCHEMA VALIDATION ENGINE (kernel plan M3, M3-P1 step 8;
|
|
3
|
+
* DR-0013).
|
|
4
|
+
*
|
|
5
|
+
* DR-0013 decided this module's substance and it is not a set of defaults to
|
|
6
|
+
* be revisited casually. Ajv 8.20.0 exact, JSON Schema Draft 2020-12, strict
|
|
7
|
+
* mode, all errors, schema and meta-schema validation, and NO coercion, NO
|
|
8
|
+
* inserted defaults, NO removal of additional properties, NO mutation of the
|
|
9
|
+
* validated input and NO automatic loading of remote schemas. Unknown or
|
|
10
|
+
* invalidly combined keywords fail schema COMPILATION, before any instance is
|
|
11
|
+
* examined.
|
|
12
|
+
*
|
|
13
|
+
* AJV IS AN INTERNAL IMPLEMENTATION DETAIL (DR-0013 clause 5). Its wording is
|
|
14
|
+
* never a public contract. Every Ajv error is mapped, by keyword, into the
|
|
15
|
+
* Tiphys diagnostic contract
|
|
16
|
+
*
|
|
17
|
+
* INVALID <json-pointer> <message>
|
|
18
|
+
*
|
|
19
|
+
* with a deterministic order. The order is a FINAL SORT by (pointer, message)
|
|
20
|
+
* applied to the collected list, not a property of the traversal, so a future
|
|
21
|
+
* engine that walks differently still emits the same lines in the same order.
|
|
22
|
+
* That is the same rule src/gates/validate.ts stated for M2 and it is
|
|
23
|
+
* deliberately identical: the two modules now share this one engine.
|
|
24
|
+
*
|
|
25
|
+
* THE MESSAGE TABLE IS THE CONTRACT, not Ajv's `message` field. A keyword
|
|
26
|
+
* whose Ajv error reaches `renderAjvError` and matches no `case` of that
|
|
27
|
+
* function's switch falls to its `default:` arm, which is a Tiphys DEFECT and
|
|
28
|
+
* is reported as one, naming the keyword, rather than being papered over with
|
|
29
|
+
* Ajv's own sentence. That refusal is what stops Ajv wording leaking into a
|
|
30
|
+
* public contract by omission (DR-0013 criterion 8).
|
|
31
|
+
*
|
|
32
|
+
* THIS PARAGRAPH NAMED THE WRONG MECHANISM UNTIL THE DR-0047 SWEEP (CR-VS-004).
|
|
33
|
+
* It said the refusal came from a `MESSAGE_BY_KEYWORD` set. That set existed,
|
|
34
|
+
* nothing ever read it (three references: its own declaration and two comments
|
|
35
|
+
* saying it was the contract), and its contents disagreed with
|
|
36
|
+
* `AUTHORING_VOCABULARY` in BOTH directions: `$ref`, `items`, `properties` and
|
|
37
|
+
* `then` were in the vocabulary and not in the set, `minimum`, `maximum` and
|
|
38
|
+
* `maxItems` were in the set and not in the vocabulary. A dead constant that a
|
|
39
|
+
* file's own documentation calls the contract is how the next reader edits the
|
|
40
|
+
* thing that does nothing, so it is deleted and this sentence now names the
|
|
41
|
+
* switch, which is what actually refuses.
|
|
42
|
+
*
|
|
43
|
+
* YAML IS INPUT DECODING AND IS A SEPARATE STAGE (DR-0013 YAML clause 3).
|
|
44
|
+
* `decodeDocument` decodes; `validateInstance` validates an already-decoded
|
|
45
|
+
* value. A decode failure and a validation failure are distinguishable in the
|
|
46
|
+
* diagnostic and neither produces a stack trace on any stream.
|
|
47
|
+
*
|
|
48
|
+
* PATHS THIS MODULE READS ARE NOT ITS OWN (D-M3-27, and the mechanism index's
|
|
49
|
+
* row `reading-a-path-whose-type-is-not-established`). Every read goes through
|
|
50
|
+
* `classifyEntry`, so a named pipe handed to `tiphys validate` is refused
|
|
51
|
+
* with the observed entry type instead of blocking the command forever.
|
|
52
|
+
*/
|
|
53
|
+
import type { Ajv2020 } from "ajv/dist/2020.js";
|
|
54
|
+
import type { ValidateFunction } from "ajv/dist/2020.js";
|
|
55
|
+
/** The dialect every Tiphys schema declares (DR-0013 clause 3). */
|
|
56
|
+
export declare const TIPHYS_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
57
|
+
/** The document root, in RFC 6901 URI-fragment form. */
|
|
58
|
+
export declare const ROOT_POINTER = "#";
|
|
59
|
+
export interface Diagnostic {
|
|
60
|
+
pointer: string;
|
|
61
|
+
message: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* THE DECLARED AUTHORING VOCABULARY (DR-0013 clause 7). THIS LIST IS THE SOURCE
|
|
65
|
+
* OF TRUTH. Ajv supplies Draft 2020-12 entire; this is what Tiphys schemas are
|
|
66
|
+
* ALLOWED to use, so a keyword outside it is a deliberate expansion rather than
|
|
67
|
+
* an accident. Every entry has both a positive and a negative test (validator
|
|
68
|
+
* criterion 2), asserted by `test/schemas.test.ts`, which DERIVES its cases
|
|
69
|
+
* from this array rather than from a hand-written list.
|
|
70
|
+
*
|
|
71
|
+
* `schemas/README.md` RENDERS THIS LIST FOR A HUMAN READER AND IS NOT ITSELF
|
|
72
|
+
* THE CONTRACT, and the distinction was paid for (CR-VS-004, FIND-02 of the
|
|
73
|
+
* DR-0047 sweep). The sentence here used to say the vocabulary IS "documented
|
|
74
|
+
* in `schemas/README.md`", which reads as a guarantee that the two agree.
|
|
75
|
+
* Measured at the swept head they did not: this array holds sixteen keywords
|
|
76
|
+
* and that document's table declared fifteen, `uniqueItems` being the missing
|
|
77
|
+
* row, since the day M3-P1 wrote both. Nothing asserted the relation, so no gate
|
|
78
|
+
* could see it.
|
|
79
|
+
*
|
|
80
|
+
* THE DRIFT IS NOW CLOSED, AND BY THE TEST RATHER THAN BY THE ROW (DR-0047
|
|
81
|
+
* sweep round 2). The row was added to `schemas/README.md`, which closes the one
|
|
82
|
+
* instance; what closes the MECHANISM is that test/schemas.test.ts:340 parses
|
|
83
|
+
* that document's keyword column and compares it to this array, so the two
|
|
84
|
+
* halves can no longer disagree silently. The document still does not DECIDE
|
|
85
|
+
* anything: this array is the contract and that one renders it, which is why
|
|
86
|
+
* the comparison is written as an assertion about the document rather than as a
|
|
87
|
+
* generator.
|
|
88
|
+
*
|
|
89
|
+
* AND THE SECOND HALF OF THAT SENTENCE IS GONE. It said every entry "therefore
|
|
90
|
+
* also has an entry in `MESSAGE_BY_KEYWORD`", which was false in both
|
|
91
|
+
* directions and about a constant nothing read; see the module header.
|
|
92
|
+
*/
|
|
93
|
+
export declare const AUTHORING_VOCABULARY: readonly string[];
|
|
94
|
+
/** Annotations that carry no constraint and are permitted everywhere. */
|
|
95
|
+
export declare const ANNOTATION_KEYS: readonly string[];
|
|
96
|
+
/** Render a value the way every diagnostic in this contract renders one. */
|
|
97
|
+
export declare function render(value: unknown): string;
|
|
98
|
+
/** The JSON type name of a value, as the `type` keyword uses it. */
|
|
99
|
+
export declare function jsonTypeOf(value: unknown): string;
|
|
100
|
+
/**
|
|
101
|
+
* THE MESSAGE CONTRACT. One entry per way a value can fail. The first eight
|
|
102
|
+
* entries are M2's, copied verbatim from `src/gates/validate.ts`'s
|
|
103
|
+
* `DIAGNOSTIC_MESSAGES` because DR-0013 clause 6 retires that ENGINE while
|
|
104
|
+
* preserving its diagnostic contract: an M2 test asserting
|
|
105
|
+
* `required property id is missing` must still read exactly that after the
|
|
106
|
+
* swap, and that is checked by re-running M2's tests unchanged rather than by
|
|
107
|
+
* this comment.
|
|
108
|
+
*/
|
|
109
|
+
export declare const DIAGNOSTIC_MESSAGES: {
|
|
110
|
+
type: (expected: string, observed: string) => string;
|
|
111
|
+
required: (name: string) => string;
|
|
112
|
+
additionalProperties: (name: string) => string;
|
|
113
|
+
enum: (value: string, permitted: string) => string;
|
|
114
|
+
const: (value: string, required: string) => string;
|
|
115
|
+
minimum: (value: string, minimum: string) => string;
|
|
116
|
+
minItems: (count: string, minimum: string) => string;
|
|
117
|
+
pattern: (value: string, pattern: string) => string;
|
|
118
|
+
minLength: (value: string, minimum: string) => string;
|
|
119
|
+
uniqueItems: (first: string, second: string) => string;
|
|
120
|
+
contains: (minimum: string) => string;
|
|
121
|
+
containsValue: (value: string, minimum: string) => string;
|
|
122
|
+
oneOf: () => string;
|
|
123
|
+
ifThen: () => string;
|
|
124
|
+
maximum: (value: string, maximum: string) => string;
|
|
125
|
+
maxItems: (count: string, maximum: string) => string;
|
|
126
|
+
cyclicRef: (reference: string) => string;
|
|
127
|
+
unresolvedRef: (reference: string) => string;
|
|
128
|
+
unknownKeyword: (keyword: string) => string;
|
|
129
|
+
strictPolicyUntyped: (keyword: string, expected: string) => string;
|
|
130
|
+
strictPolicy: () => string;
|
|
131
|
+
remoteRef: (reference: string) => string;
|
|
132
|
+
invalidSchemaDocument: () => string;
|
|
133
|
+
patternUncompilable: () => string;
|
|
134
|
+
cyclicCompilation: () => string;
|
|
135
|
+
/** Nothing above matched. Deliberately carries no Ajv text at all. */
|
|
136
|
+
uncompilable: () => string;
|
|
137
|
+
/** A keyword Ajv reported that this table does not translate. */
|
|
138
|
+
untranslated: (keyword: string) => string;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* `INVALID <json-pointer> <message>`. The public contract, shared with
|
|
142
|
+
* `src/gates/validate.ts`.
|
|
143
|
+
*
|
|
144
|
+
* Pointers are RFC 6901 JSON Pointers in URI-fragment form, so the document
|
|
145
|
+
* root is `#` and a nested location is `#/gates/0/id`. The fragment form is
|
|
146
|
+
* chosen because the bare pointer to the root is the EMPTY STRING, which
|
|
147
|
+
* would render this line with two consecutive spaces.
|
|
148
|
+
*/
|
|
149
|
+
export declare function formatDiagnostic(diagnostic: Diagnostic): string;
|
|
150
|
+
export declare function formatDiagnostics(diagnostics: Diagnostic[]): string[];
|
|
151
|
+
/** The contract's deterministic order: by pointer, then by message, ASCII. */
|
|
152
|
+
export declare function sortDiagnostics(diagnostics: Diagnostic[]): Diagnostic[];
|
|
153
|
+
export type SchemaDocument = Record<string, unknown>;
|
|
154
|
+
/**
|
|
155
|
+
* A fresh Ajv, configured EXACTLY as DR-0013 clause 4 lists. Each policy is
|
|
156
|
+
* named so a diff that removes one is visible as the removal of a decided
|
|
157
|
+
* policy rather than as a formatting change.
|
|
158
|
+
*/
|
|
159
|
+
export declare function makeAjv(): Ajv2020;
|
|
160
|
+
/** A compiled validator, or the reason the schema could not be compiled. */
|
|
161
|
+
export type Compilation = {
|
|
162
|
+
ok: true;
|
|
163
|
+
validator: ValidateFunction;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* `diagnostics` is the contract; `reason` is the same thing rendered as one
|
|
167
|
+
* line, kept because callers that only want to print something want a
|
|
168
|
+
* string. Both are Tiphys-owned: no Ajv sentence reaches either.
|
|
169
|
+
*/
|
|
170
|
+
| {
|
|
171
|
+
ok: false;
|
|
172
|
+
diagnostics: Diagnostic[];
|
|
173
|
+
reason: string;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Compile a schema. Cached by schema OBJECT IDENTITY, because a fresh Ajv per
|
|
177
|
+
* call is both slow and wrong: two schemas carrying the same `$id` cannot
|
|
178
|
+
* share one Ajv instance, so each schema gets its own.
|
|
179
|
+
*
|
|
180
|
+
* IDENTITY, NOT CONTENT, and it has one observable consequence worth stating
|
|
181
|
+
* rather than discovering. A caller that MUTATES a schema object in place
|
|
182
|
+
* after compiling it keeps the old validator, because the WeakMap key is
|
|
183
|
+
* unchanged. That is correct for how schemas are used here (documents read
|
|
184
|
+
* from disk and never edited), and it was measured: the first attempt at
|
|
185
|
+
* criterion 4's red witness defanged a keyword in place and the diagnostics
|
|
186
|
+
* did not move, which read exactly like a schema whose keyword was doing
|
|
187
|
+
* nothing. The witness was redone with a schema re-read from disk per arm.
|
|
188
|
+
* Any future caller that wants to compile a modified schema must hand over a
|
|
189
|
+
* NEW object.
|
|
190
|
+
*
|
|
191
|
+
* The cache is keyed by the schema object AND by the COMPANION LIST, also by
|
|
192
|
+
* identity: the same document compiled with and without a companion is two
|
|
193
|
+
* different compilations, and returning the first for the second would be the
|
|
194
|
+
* quiet wrong answer. Companion lists here are one or zero long, so the scan
|
|
195
|
+
* is a scan of a one-element array.
|
|
196
|
+
*/
|
|
197
|
+
export declare function compileSchema(schema: SchemaDocument, companions?: readonly SchemaDocument[]): Compilation;
|
|
198
|
+
/**
|
|
199
|
+
* Translate a COMPILATION failure into the Tiphys diagnostic contract.
|
|
200
|
+
*
|
|
201
|
+
* The classification reads Ajv's exception only to EXTRACT IDENTIFIERS from
|
|
202
|
+
* it; nothing Ajv wrote is passed through. An unrecognised shape becomes the
|
|
203
|
+
* bare `schema could not be compiled`, which loses detail deliberately:
|
|
204
|
+
* losing detail is recoverable, and leaking a third party's wording into a
|
|
205
|
+
* contract nine phases are written against is not.
|
|
206
|
+
*/
|
|
207
|
+
export declare function compilationDiagnostics(error: unknown, ajv: Ajv2020, schema: SchemaDocument): Diagnostic[];
|
|
208
|
+
/**
|
|
209
|
+
* Validate an already-decoded instance against a schema document.
|
|
210
|
+
*
|
|
211
|
+
* Returns diagnostics in the contract's deterministic order. A schema that
|
|
212
|
+
* cannot be compiled yields ONE root diagnostic rather than a throw: this
|
|
213
|
+
* function is called from a CLI whose ordinary input is hand-authored, and a
|
|
214
|
+
* thrown compile error there is the stack trace step 8b exists to prevent.
|
|
215
|
+
*/
|
|
216
|
+
export declare function validateInstance(schema: SchemaDocument, instance: unknown, companions?: readonly SchemaDocument[]): Diagnostic[];
|
|
217
|
+
/** Validate and format in one step. */
|
|
218
|
+
export declare function validateToLines(schema: SchemaDocument, instance: unknown, companions?: readonly SchemaDocument[]): string[];
|
|
219
|
+
/**
|
|
220
|
+
* A decode outcome. A DECODE failure and a VALIDATION failure are different
|
|
221
|
+
* things and the caller can tell which it has, which is the separation
|
|
222
|
+
* DR-0013 YAML clause 3 requires.
|
|
223
|
+
*/
|
|
224
|
+
export type DecodeResult = {
|
|
225
|
+
ok: true;
|
|
226
|
+
value: unknown;
|
|
227
|
+
} | {
|
|
228
|
+
ok: false;
|
|
229
|
+
reason: string;
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Decode YAML (or JSON, which YAML 1.2 is a superset of) into a plain value.
|
|
233
|
+
*
|
|
234
|
+
* A parse failure returns a reason, never a throw and never a stack trace.
|
|
235
|
+
* `yaml` reports position information; it is collapsed to one line because
|
|
236
|
+
* the contract is one diagnostic line per failure.
|
|
237
|
+
*/
|
|
238
|
+
export declare function decodeDocument(text: string, label: string): DecodeResult;
|
|
239
|
+
/** What a guarded read of an operator-supplied path produced. */
|
|
240
|
+
export type GuardedRead = {
|
|
241
|
+
ok: true;
|
|
242
|
+
body: string;
|
|
243
|
+
} | {
|
|
244
|
+
ok: false;
|
|
245
|
+
reason: string;
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* READ A PATH THIS PROGRAM DID NOT CREATE (D-M3-27).
|
|
249
|
+
*
|
|
250
|
+
* `tiphys validate <file>` takes an operator-supplied path, so the path may
|
|
251
|
+
* be a FIFO, a directory, a socket or a dangling symlink. Opening a FIFO with
|
|
252
|
+
* no writer blocks forever and the command never returns, which is the M1-P5
|
|
253
|
+
* class (CR-520). The type is established with `classifyEntry` BEFORE the
|
|
254
|
+
* open, and an irregular entry is refused naming the path and the observed
|
|
255
|
+
* type.
|
|
256
|
+
*/
|
|
257
|
+
export declare function readOperatorPath(path: string): GuardedRead;
|
|
258
|
+
/**
|
|
259
|
+
* Establish that a path is a DIRECTORY before walking it. Same hazard, same
|
|
260
|
+
* rule: `--context` is operator-supplied and a FIFO there would block the
|
|
261
|
+
* walk exactly as it blocks a read.
|
|
262
|
+
*/
|
|
263
|
+
export declare function classifyContextDirectory(path: string): string | undefined;
|