@tiphys/kernel 0.0.0 → 0.1.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.
Files changed (181) hide show
  1. package/AGENTS.md +611 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +278 -0
  4. package/checklists/clean-room.yaml +325 -0
  5. package/checklists/env-failure-diagnosis.yaml +68 -0
  6. package/checklists/flake-playbook.yaml +68 -0
  7. package/checklists/hazard-review.yaml +144 -0
  8. package/checklists/plan-review.yaml +103 -0
  9. package/dist/bin/tiphys.d.ts +2 -0
  10. package/dist/bin/tiphys.js +14 -0
  11. package/dist/src/brief.d.ts +32 -0
  12. package/dist/src/brief.js +59 -0
  13. package/dist/src/checklists.d.ts +169 -0
  14. package/dist/src/checklists.js +310 -0
  15. package/dist/src/checks.d.ts +828 -0
  16. package/dist/src/checks.js +3314 -0
  17. package/dist/src/cli.d.ts +9 -0
  18. package/dist/src/cli.js +57 -0
  19. package/dist/src/commands/brief.d.ts +92 -0
  20. package/dist/src/commands/brief.js +320 -0
  21. package/dist/src/commands/checklist.d.ts +42 -0
  22. package/dist/src/commands/checklist.js +168 -0
  23. package/dist/src/commands/doctor.d.ts +35 -0
  24. package/dist/src/commands/doctor.js +605 -0
  25. package/dist/src/commands/gates.d.ts +9 -0
  26. package/dist/src/commands/gates.js +360 -0
  27. package/dist/src/commands/init.d.ts +30 -0
  28. package/dist/src/commands/init.js +128 -0
  29. package/dist/src/commands/lock.d.ts +1 -0
  30. package/dist/src/commands/lock.js +229 -0
  31. package/dist/src/commands/mode.d.ts +40 -0
  32. package/dist/src/commands/mode.js +134 -0
  33. package/dist/src/commands/plan.d.ts +20 -0
  34. package/dist/src/commands/plan.js +105 -0
  35. package/dist/src/commands/pool.d.ts +1 -0
  36. package/dist/src/commands/pool.js +128 -0
  37. package/dist/src/commands/spawn.d.ts +1 -0
  38. package/dist/src/commands/spawn.js +146 -0
  39. package/dist/src/commands/status.d.ts +19 -0
  40. package/dist/src/commands/status.js +120 -0
  41. package/dist/src/commands/teardown.d.ts +1 -0
  42. package/dist/src/commands/teardown.js +79 -0
  43. package/dist/src/commands/tuition.d.ts +21 -0
  44. package/dist/src/commands/tuition.js +218 -0
  45. package/dist/src/commands/validate.d.ts +78 -0
  46. package/dist/src/commands/validate.js +360 -0
  47. package/dist/src/commands/watch.d.ts +1 -0
  48. package/dist/src/commands/watch.js +172 -0
  49. package/dist/src/exec/env.d.ts +128 -0
  50. package/dist/src/exec/env.js +190 -0
  51. package/dist/src/fleet.d.ts +51 -0
  52. package/dist/src/fleet.js +80 -0
  53. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  54. package/dist/src/gates/adapters/http-json.js +283 -0
  55. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  56. package/dist/src/gates/adapters/migrations-command.js +373 -0
  57. package/dist/src/gates/citations.d.ts +408 -0
  58. package/dist/src/gates/citations.js +1163 -0
  59. package/dist/src/gates/coverage.d.ts +284 -0
  60. package/dist/src/gates/coverage.js +701 -0
  61. package/dist/src/gates/credentials.d.ts +74 -0
  62. package/dist/src/gates/credentials.js +533 -0
  63. package/dist/src/gates/deploy.d.ts +1 -0
  64. package/dist/src/gates/deploy.js +33 -0
  65. package/dist/src/gates/manifest.d.ts +99 -0
  66. package/dist/src/gates/manifest.js +208 -0
  67. package/dist/src/gates/migrations.d.ts +1 -0
  68. package/dist/src/gates/migrations.js +36 -0
  69. package/dist/src/gates/pin.d.ts +114 -0
  70. package/dist/src/gates/pin.js +154 -0
  71. package/dist/src/gates/red-witness.d.ts +22 -0
  72. package/dist/src/gates/red-witness.js +390 -0
  73. package/dist/src/gates/release.d.ts +283 -0
  74. package/dist/src/gates/release.js +820 -0
  75. package/dist/src/gates/result.d.ts +116 -0
  76. package/dist/src/gates/result.js +91 -0
  77. package/dist/src/gates/run.d.ts +566 -0
  78. package/dist/src/gates/run.js +1536 -0
  79. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  80. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  81. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  82. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  83. package/dist/src/gates/schemas/phase-declaration.schema.json +42 -0
  84. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  85. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  86. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  87. package/dist/src/gates/scope.d.ts +131 -0
  88. package/dist/src/gates/scope.js +1018 -0
  89. package/dist/src/gates/suite.d.ts +217 -0
  90. package/dist/src/gates/suite.js +927 -0
  91. package/dist/src/gates/validate.d.ts +121 -0
  92. package/dist/src/gates/validate.js +414 -0
  93. package/dist/src/hooks.d.ts +32 -0
  94. package/dist/src/hooks.js +62 -0
  95. package/dist/src/liveness.d.ts +321 -0
  96. package/dist/src/liveness.js +396 -0
  97. package/dist/src/lock.d.ts +178 -0
  98. package/dist/src/lock.js +500 -0
  99. package/dist/src/modes.d.ts +149 -0
  100. package/dist/src/modes.js +258 -0
  101. package/dist/src/path-identity.d.ts +2 -0
  102. package/dist/src/path-identity.js +10 -0
  103. package/dist/src/plan.d.ts +73 -0
  104. package/dist/src/plan.js +153 -0
  105. package/dist/src/pool.d.ts +130 -0
  106. package/dist/src/pool.js +721 -0
  107. package/dist/src/roles.d.ts +430 -0
  108. package/dist/src/roles.js +734 -0
  109. package/dist/src/spawn.d.ts +177 -0
  110. package/dist/src/spawn.js +332 -0
  111. package/dist/src/status.d.ts +91 -0
  112. package/dist/src/status.js +119 -0
  113. package/dist/src/task.d.ts +264 -0
  114. package/dist/src/task.js +305 -0
  115. package/dist/src/teardown.d.ts +32 -0
  116. package/dist/src/teardown.js +314 -0
  117. package/dist/src/tuition.d.ts +159 -0
  118. package/dist/src/tuition.js +311 -0
  119. package/dist/src/validate.d.ts +230 -0
  120. package/dist/src/validate.js +732 -0
  121. package/dist/src/version.d.ts +3 -0
  122. package/dist/src/version.js +38 -0
  123. package/dist/src/watcher.d.ts +275 -0
  124. package/dist/src/watcher.js +859 -0
  125. package/dist/src/witness/run.d.ts +274 -0
  126. package/dist/src/witness/run.js +1327 -0
  127. package/dist/src/witness/spec.d.ts +102 -0
  128. package/dist/src/witness/spec.js +253 -0
  129. package/dist/tsconfig.src.tsbuildinfo +1 -0
  130. package/gate-registry.yaml +390 -0
  131. package/gates.manifest.json +195 -0
  132. package/package.json +57 -3
  133. package/role-model-config.yaml +88 -0
  134. package/roles/README.md +128 -0
  135. package/roles/_shared-dispatch-contract.md +87 -0
  136. package/roles/adversarial-plan-reviewer.md +80 -0
  137. package/roles/clean-room-reviewer.md +140 -0
  138. package/roles/implementer.md +460 -0
  139. package/roles/investigator.md +138 -0
  140. package/roles/plan-writer.md +95 -0
  141. package/schemas/README.md +81 -0
  142. package/schemas/assurance-modes.schema.json +264 -0
  143. package/schemas/charter.schema.json +166 -0
  144. package/schemas/checklist.schema.json +114 -0
  145. package/schemas/decision-record.schema.json +88 -0
  146. package/schemas/final-report.schema.json +90 -0
  147. package/schemas/finding.schema.json +106 -0
  148. package/schemas/gate-registry.schema.json +260 -0
  149. package/schemas/mechanism-index.schema.json +94 -0
  150. package/schemas/plan.schema.json +300 -0
  151. package/schemas/report.schema.json +579 -0
  152. package/schemas/role-brief.schema.json +105 -0
  153. package/schemas/role-model-config.schema.json +90 -0
  154. package/schemas/status-line.schema.json +40 -0
  155. package/schemas/tuition.schema.json +191 -0
  156. package/schemas/verdict.schema.json +289 -0
  157. package/schemas/work-history.schema.json +183 -0
  158. package/templates/charter.example.yaml +54 -0
  159. package/templates/decision-record.example.yaml +27 -0
  160. package/templates/final-report.example.yaml +80 -0
  161. package/templates/plan.example.yaml +87 -0
  162. package/templates/report.example.yaml +236 -0
  163. package/templates/warnings.md +74 -0
  164. package/templates/work-history.example.yaml +185 -0
  165. package/tuition/README.md +76 -0
  166. package/tuition/T-001.yaml +48 -0
  167. package/tuition/T-002.yaml +51 -0
  168. package/tuition/T-003.yaml +100 -0
  169. package/tuition/T-004.yaml +52 -0
  170. package/tuition/T-005.yaml +72 -0
  171. package/tuition/T-006.yaml +81 -0
  172. package/tuition/T-007.yaml +56 -0
  173. package/tuition/T-008.yaml +111 -0
  174. package/tuition/T-009.yaml +50 -0
  175. package/tuition/T-015.yaml +36 -0
  176. package/tuition/T-016.yaml +36 -0
  177. package/tuition/T-017.yaml +46 -0
  178. package/tuition/T-018.yaml +84 -0
  179. package/tuition/T-021.yaml +40 -0
  180. package/tuition/T-022.yaml +36 -0
  181. 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,230 @@
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` without an entry in
27
+ * `MESSAGE_BY_KEYWORD` is a Tiphys DEFECT and is reported as one, naming the
28
+ * keyword, rather than being papered over with Ajv's own sentence. That
29
+ * refusal is what stops Ajv wording leaking into a public contract by
30
+ * omission (DR-0013 criterion 8).
31
+ *
32
+ * YAML IS INPUT DECODING AND IS A SEPARATE STAGE (DR-0013 YAML clause 3).
33
+ * `decodeDocument` decodes; `validateInstance` validates an already-decoded
34
+ * value. A decode failure and a validation failure are distinguishable in the
35
+ * diagnostic and neither produces a stack trace on any stream.
36
+ *
37
+ * PATHS THIS MODULE READS ARE NOT ITS OWN (D-M3-27, and the mechanism index's
38
+ * row `reading-a-path-whose-type-is-not-established`). Every read goes through
39
+ * `classifyEntry`, so a named pipe handed to `tiphys validate` is refused
40
+ * with the observed entry type instead of blocking the command forever.
41
+ */
42
+ import type { Ajv2020 } from "ajv/dist/2020.js";
43
+ import type { ValidateFunction } from "ajv/dist/2020.js";
44
+ /** The dialect every Tiphys schema declares (DR-0013 clause 3). */
45
+ export declare const TIPHYS_DIALECT = "https://json-schema.org/draft/2020-12/schema";
46
+ /** The document root, in RFC 6901 URI-fragment form. */
47
+ export declare const ROOT_POINTER = "#";
48
+ export interface Diagnostic {
49
+ pointer: string;
50
+ message: string;
51
+ }
52
+ /**
53
+ * THE DECLARED AUTHORING VOCABULARY (DR-0013 clause 7), documented in
54
+ * `schemas/README.md` and asserted by `test/schemas.test.ts`. Ajv supplies
55
+ * Draft 2020-12 entire; this list is what Tiphys schemas are ALLOWED to use,
56
+ * so a keyword outside it is a deliberate expansion rather than an accident.
57
+ * Every entry has both a positive and a negative test (validator criterion 2)
58
+ * and therefore also has an entry in `MESSAGE_BY_KEYWORD`.
59
+ */
60
+ export declare const AUTHORING_VOCABULARY: readonly string[];
61
+ /** Annotations that carry no constraint and are permitted everywhere. */
62
+ export declare const ANNOTATION_KEYS: readonly string[];
63
+ /** Render a value the way every diagnostic in this contract renders one. */
64
+ export declare function render(value: unknown): string;
65
+ /** The JSON type name of a value, as the `type` keyword uses it. */
66
+ export declare function jsonTypeOf(value: unknown): string;
67
+ /**
68
+ * THE MESSAGE CONTRACT. One entry per way a value can fail. The first eight
69
+ * entries are M2's, copied verbatim from `src/gates/validate.ts`'s
70
+ * `DIAGNOSTIC_MESSAGES` because DR-0013 clause 6 retires that ENGINE while
71
+ * preserving its diagnostic contract: an M2 test asserting
72
+ * `required property id is missing` must still read exactly that after the
73
+ * swap, and that is checked by re-running M2's tests unchanged rather than by
74
+ * this comment.
75
+ */
76
+ export declare const DIAGNOSTIC_MESSAGES: {
77
+ type: (expected: string, observed: string) => string;
78
+ required: (name: string) => string;
79
+ additionalProperties: (name: string) => string;
80
+ enum: (value: string, permitted: string) => string;
81
+ const: (value: string, required: string) => string;
82
+ minimum: (value: string, minimum: string) => string;
83
+ minItems: (count: string, minimum: string) => string;
84
+ pattern: (value: string, pattern: string) => string;
85
+ minLength: (value: string, minimum: string) => string;
86
+ uniqueItems: (first: string, second: string) => string;
87
+ contains: (minimum: string) => string;
88
+ containsValue: (value: string, minimum: string) => string;
89
+ oneOf: () => string;
90
+ ifThen: () => string;
91
+ maximum: (value: string, maximum: string) => string;
92
+ maxItems: (count: string, maximum: string) => string;
93
+ cyclicRef: (reference: string) => string;
94
+ unresolvedRef: (reference: string) => string;
95
+ unknownKeyword: (keyword: string) => string;
96
+ strictPolicyUntyped: (keyword: string, expected: string) => string;
97
+ strictPolicy: () => string;
98
+ remoteRef: (reference: string) => string;
99
+ invalidSchemaDocument: () => string;
100
+ patternUncompilable: () => string;
101
+ cyclicCompilation: () => string;
102
+ /** Nothing above matched. Deliberately carries no Ajv text at all. */
103
+ uncompilable: () => string;
104
+ /** A keyword Ajv reported that this table does not translate. */
105
+ untranslated: (keyword: string) => string;
106
+ };
107
+ /**
108
+ * `INVALID <json-pointer> <message>`. The public contract, shared with
109
+ * `src/gates/validate.ts`.
110
+ *
111
+ * Pointers are RFC 6901 JSON Pointers in URI-fragment form, so the document
112
+ * root is `#` and a nested location is `#/gates/0/id`. The fragment form is
113
+ * chosen because the bare pointer to the root is the EMPTY STRING, which
114
+ * would render this line with two consecutive spaces.
115
+ */
116
+ export declare function formatDiagnostic(diagnostic: Diagnostic): string;
117
+ export declare function formatDiagnostics(diagnostics: Diagnostic[]): string[];
118
+ /** The contract's deterministic order: by pointer, then by message, ASCII. */
119
+ export declare function sortDiagnostics(diagnostics: Diagnostic[]): Diagnostic[];
120
+ export type SchemaDocument = Record<string, unknown>;
121
+ /**
122
+ * A fresh Ajv, configured EXACTLY as DR-0013 clause 4 lists. Each policy is
123
+ * named so a diff that removes one is visible as the removal of a decided
124
+ * policy rather than as a formatting change.
125
+ */
126
+ export declare function makeAjv(): Ajv2020;
127
+ /** A compiled validator, or the reason the schema could not be compiled. */
128
+ export type Compilation = {
129
+ ok: true;
130
+ validator: ValidateFunction;
131
+ }
132
+ /**
133
+ * `diagnostics` is the contract; `reason` is the same thing rendered as one
134
+ * line, kept because callers that only want to print something want a
135
+ * string. Both are Tiphys-owned: no Ajv sentence reaches either.
136
+ */
137
+ | {
138
+ ok: false;
139
+ diagnostics: Diagnostic[];
140
+ reason: string;
141
+ };
142
+ /**
143
+ * Compile a schema. Cached by schema OBJECT IDENTITY, because a fresh Ajv per
144
+ * call is both slow and wrong: two schemas carrying the same `$id` cannot
145
+ * share one Ajv instance, so each schema gets its own.
146
+ *
147
+ * IDENTITY, NOT CONTENT, and it has one observable consequence worth stating
148
+ * rather than discovering. A caller that MUTATES a schema object in place
149
+ * after compiling it keeps the old validator, because the WeakMap key is
150
+ * unchanged. That is correct for how schemas are used here (documents read
151
+ * from disk and never edited), and it was measured: the first attempt at
152
+ * criterion 4's red witness defanged a keyword in place and the diagnostics
153
+ * did not move, which read exactly like a schema whose keyword was doing
154
+ * nothing. The witness was redone with a schema re-read from disk per arm.
155
+ * Any future caller that wants to compile a modified schema must hand over a
156
+ * NEW object.
157
+ *
158
+ * The cache is keyed by the schema object AND by the COMPANION LIST, also by
159
+ * identity: the same document compiled with and without a companion is two
160
+ * different compilations, and returning the first for the second would be the
161
+ * quiet wrong answer. Companion lists here are one or zero long, so the scan
162
+ * is a scan of a one-element array.
163
+ */
164
+ export declare function compileSchema(schema: SchemaDocument, companions?: readonly SchemaDocument[]): Compilation;
165
+ /**
166
+ * Translate a COMPILATION failure into the Tiphys diagnostic contract.
167
+ *
168
+ * The classification reads Ajv's exception only to EXTRACT IDENTIFIERS from
169
+ * it; nothing Ajv wrote is passed through. An unrecognised shape becomes the
170
+ * bare `schema could not be compiled`, which loses detail deliberately:
171
+ * losing detail is recoverable, and leaking a third party's wording into a
172
+ * contract nine phases are written against is not.
173
+ */
174
+ export declare function compilationDiagnostics(error: unknown, ajv: Ajv2020, schema: SchemaDocument): Diagnostic[];
175
+ /**
176
+ * Validate an already-decoded instance against a schema document.
177
+ *
178
+ * Returns diagnostics in the contract's deterministic order. A schema that
179
+ * cannot be compiled yields ONE root diagnostic rather than a throw: this
180
+ * function is called from a CLI whose ordinary input is hand-authored, and a
181
+ * thrown compile error there is the stack trace step 8b exists to prevent.
182
+ */
183
+ export declare function validateInstance(schema: SchemaDocument, instance: unknown, companions?: readonly SchemaDocument[]): Diagnostic[];
184
+ /** Validate and format in one step. */
185
+ export declare function validateToLines(schema: SchemaDocument, instance: unknown, companions?: readonly SchemaDocument[]): string[];
186
+ /**
187
+ * A decode outcome. A DECODE failure and a VALIDATION failure are different
188
+ * things and the caller can tell which it has, which is the separation
189
+ * DR-0013 YAML clause 3 requires.
190
+ */
191
+ export type DecodeResult = {
192
+ ok: true;
193
+ value: unknown;
194
+ } | {
195
+ ok: false;
196
+ reason: string;
197
+ };
198
+ /**
199
+ * Decode YAML (or JSON, which YAML 1.2 is a superset of) into a plain value.
200
+ *
201
+ * A parse failure returns a reason, never a throw and never a stack trace.
202
+ * `yaml` reports position information; it is collapsed to one line because
203
+ * the contract is one diagnostic line per failure.
204
+ */
205
+ export declare function decodeDocument(text: string, label: string): DecodeResult;
206
+ /** What a guarded read of an operator-supplied path produced. */
207
+ export type GuardedRead = {
208
+ ok: true;
209
+ body: string;
210
+ } | {
211
+ ok: false;
212
+ reason: string;
213
+ };
214
+ /**
215
+ * READ A PATH THIS PROGRAM DID NOT CREATE (D-M3-27).
216
+ *
217
+ * `tiphys validate <file>` takes an operator-supplied path, so the path may
218
+ * be a FIFO, a directory, a socket or a dangling symlink. Opening a FIFO with
219
+ * no writer blocks forever and the command never returns, which is the M1-P5
220
+ * class (CR-520). The type is established with `classifyEntry` BEFORE the
221
+ * open, and an irregular entry is refused naming the path and the observed
222
+ * type.
223
+ */
224
+ export declare function readOperatorPath(path: string): GuardedRead;
225
+ /**
226
+ * Establish that a path is a DIRECTORY before walking it. Same hazard, same
227
+ * rule: `--context` is operator-supplied and a FIFO there would block the
228
+ * walk exactly as it blocks a read.
229
+ */
230
+ export declare function classifyContextDirectory(path: string): string | undefined;