@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,732 @@
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 { readFileSync, statSync } from "node:fs";
43
+ import { createRequire } from "node:module";
44
+ import { classifyEntry } from "./task.js";
45
+ /**
46
+ * THE TWO PRODUCTION DEPENDENCIES ARE LOADED LAZILY, AND THIS IS NOT A
47
+ * MICRO-OPTIMISATION. Measured 2026-08-08 while retiring the M2 engine.
48
+ *
49
+ * Three delivered M2 tests COPY the kernel's own tree to a scratch location
50
+ * OUTSIDE the repository and run it there: `stagedDist` in
51
+ * `test/gates.test.ts` copies `dist/`, and `copyInstallation` in
52
+ * `test/scope-gate.test.ts` copies `src/`. Node resolves a bare specifier by
53
+ * walking `node_modules` UPWARD from the importing file, and a copy under
54
+ * `/tmp` has no `node_modules` above it. A top-level `import ... from "ajv"`
55
+ * in this module therefore made three previously-passing M2 tests fail with
56
+ * `ERR_MODULE_NOT_FOUND` at module load, exit code 1, before any of the
57
+ * conditions those tests exist to exercise could happen. Captured in the
58
+ * work history.
59
+ *
60
+ * Those tests are re-run UNCHANGED under DR-0013 clause 6 and may not be
61
+ * edited, so the module must not need `ajv` resolvable merely to be
62
+ * IMPORTED. `createRequire` defers the resolution to the first schema
63
+ * compilation, which a copied-out gate that fails earlier never reaches, and
64
+ * an unresolvable dependency then becomes a compile diagnostic rather than an
65
+ * uncaught crash read as a verdict (the CR-1047 property, one level up).
66
+ */
67
+ const requireDependency = createRequire(import.meta.url);
68
+ function ajvModule() {
69
+ return requireDependency("ajv/dist/2020.js");
70
+ }
71
+ function yamlModule() {
72
+ return requireDependency("yaml");
73
+ }
74
+ /** The dialect every Tiphys schema declares (DR-0013 clause 3). */
75
+ export const TIPHYS_DIALECT = "https://json-schema.org/draft/2020-12/schema";
76
+ /** The document root, in RFC 6901 URI-fragment form. */
77
+ export const ROOT_POINTER = "#";
78
+ /**
79
+ * THE DECLARED AUTHORING VOCABULARY (DR-0013 clause 7), documented in
80
+ * `schemas/README.md` and asserted by `test/schemas.test.ts`. Ajv supplies
81
+ * Draft 2020-12 entire; this list is what Tiphys schemas are ALLOWED to use,
82
+ * so a keyword outside it is a deliberate expansion rather than an accident.
83
+ * Every entry has both a positive and a negative test (validator criterion 2)
84
+ * and therefore also has an entry in `MESSAGE_BY_KEYWORD`.
85
+ */
86
+ export const AUTHORING_VOCABULARY = [
87
+ "$ref",
88
+ "additionalProperties",
89
+ "const",
90
+ "contains",
91
+ "enum",
92
+ "if",
93
+ "items",
94
+ "minItems",
95
+ "minLength",
96
+ "oneOf",
97
+ "pattern",
98
+ "properties",
99
+ "required",
100
+ "then",
101
+ "type",
102
+ "uniqueItems",
103
+ ];
104
+ /** Annotations that carry no constraint and are permitted everywhere. */
105
+ export const ANNOTATION_KEYS = [
106
+ "$comment",
107
+ "$defs",
108
+ "$id",
109
+ "$schema",
110
+ "description",
111
+ "title",
112
+ ];
113
+ /** Render a value the way every diagnostic in this contract renders one. */
114
+ export function render(value) {
115
+ return JSON.stringify(value) ?? String(value);
116
+ }
117
+ /** The JSON type name of a value, as the `type` keyword uses it. */
118
+ export function jsonTypeOf(value) {
119
+ if (value === null) {
120
+ return "null";
121
+ }
122
+ if (Array.isArray(value)) {
123
+ return "array";
124
+ }
125
+ const primitive = typeof value;
126
+ if (primitive === "number") {
127
+ return Number.isInteger(value) ? "integer" : "number";
128
+ }
129
+ if (primitive === "boolean") {
130
+ return "boolean";
131
+ }
132
+ if (primitive === "string") {
133
+ return "string";
134
+ }
135
+ return "object";
136
+ }
137
+ /**
138
+ * THE MESSAGE CONTRACT. One entry per way a value can fail. The first eight
139
+ * entries are M2's, copied verbatim from `src/gates/validate.ts`'s
140
+ * `DIAGNOSTIC_MESSAGES` because DR-0013 clause 6 retires that ENGINE while
141
+ * preserving its diagnostic contract: an M2 test asserting
142
+ * `required property id is missing` must still read exactly that after the
143
+ * swap, and that is checked by re-running M2's tests unchanged rather than by
144
+ * this comment.
145
+ */
146
+ export const DIAGNOSTIC_MESSAGES = {
147
+ type: (expected, observed) => `expected type ${expected} but found ${observed}`,
148
+ required: (name) => `required property ${name} is missing`,
149
+ additionalProperties: (name) => `property ${name} is not permitted here`,
150
+ enum: (value, permitted) => `value ${value} is not one of the permitted values ${permitted}`,
151
+ const: (value, required) => `value ${value} does not equal the required constant ${required}`,
152
+ minimum: (value, minimum) => `value ${value} is below the minimum ${minimum}`,
153
+ minItems: (count, minimum) => `array has ${count} items, fewer than the required minimum ${minimum}`,
154
+ pattern: (value, pattern) => `value ${value} does not match the required pattern ${pattern}`,
155
+ /* M3's additions to the vocabulary, Tiphys-owned wording throughout. */
156
+ minLength: (value, minimum) => `value ${value} is shorter than the required minimum length ${minimum}`,
157
+ uniqueItems: (first, second) => `array items ${first} and ${second} are duplicates and must be unique`,
158
+ contains: (minimum) => `array contains no item matching the required shape, and ${minimum} is required`,
159
+ /* CR-004 (M3-P3 fix round 1). The generic `contains` line above names the
160
+ FIELD and not the required value, so an author reading it is told an array
161
+ is missing something and not WHAT. When the `contains` subschema is a bare
162
+ `const`, the value is knowable and is named. The generic form stays for
163
+ every other shape, because a subschema with `properties` and `required`
164
+ has no single value to quote and inventing a rendering of it would be
165
+ worse than saying "the required shape". */
166
+ containsValue: (value, minimum) => `array contains no item equal to ${value}, and ${minimum} is required`,
167
+ oneOf: () => "value matches no permitted alternative here",
168
+ ifThen: () => "value does not satisfy the requirements its own shape triggers here",
169
+ maximum: (value, maximum) => `value ${value} is above the maximum ${maximum}`,
170
+ maxItems: (count, maximum) => `array has ${count} items, more than the permitted maximum ${maximum}`,
171
+ cyclicRef: (reference) => `schema reference ${reference} is cyclic`,
172
+ unresolvedRef: (reference) => `schema reference ${reference} does not resolve`,
173
+ /* ------------------------------------------------------------------ */
174
+ /* COMPILATION FAILURES ARE PART OF THE CONTRACT TOO (fix round 1, */
175
+ /* B-002). Criteria 4, 5 and 7 all fail at COMPILATION rather than at */
176
+ /* validation, and the first version of this module printed the raw */
177
+ /* exception text on that path, so Ajv's wording reached a public */
178
+ /* stream on the arm nobody had asserted about. That is T-009's shape */
179
+ /* one layer down: one witnessed arm, one unwitnessed arm, and the */
180
+ /* unwitnessed one is the one that broke. Every entry below carries */
181
+ /* only an IDENTIFIER lifted out of the Ajv error (a keyword name, a */
182
+ /* reference, a location), never an Ajv sentence. */
183
+ /* ------------------------------------------------------------------ */
184
+ unknownKeyword: (keyword) => `schema keyword ${keyword} is not in this validator's vocabulary`,
185
+ strictPolicyUntyped: (keyword, expected) => `schema uses keyword ${keyword} without declaring type ${expected}, which this validator's strict policy requires`,
186
+ strictPolicy: () => "schema is refused by this validator's strict policy",
187
+ remoteRef: (reference) => `schema reference ${reference} is remote, and this validator never loads remote schemas`,
188
+ invalidSchemaDocument: () => "schema is not a valid JSON Schema document",
189
+ patternUncompilable: () => "schema contains a pattern that is not a valid regular expression",
190
+ cyclicCompilation: () => "schema references form a cycle this validator cannot compile",
191
+ /** Nothing above matched. Deliberately carries no Ajv text at all. */
192
+ uncompilable: () => "schema could not be compiled",
193
+ /** A keyword Ajv reported that this table does not translate. */
194
+ untranslated: (keyword) => `internal defect: no Tiphys diagnostic is defined for schema keyword ${keyword}`,
195
+ };
196
+ /**
197
+ * `INVALID <json-pointer> <message>`. The public contract, shared with
198
+ * `src/gates/validate.ts`.
199
+ *
200
+ * Pointers are RFC 6901 JSON Pointers in URI-fragment form, so the document
201
+ * root is `#` and a nested location is `#/gates/0/id`. The fragment form is
202
+ * chosen because the bare pointer to the root is the EMPTY STRING, which
203
+ * would render this line with two consecutive spaces.
204
+ */
205
+ export function formatDiagnostic(diagnostic) {
206
+ return `INVALID ${diagnostic.pointer} ${diagnostic.message}`;
207
+ }
208
+ export function formatDiagnostics(diagnostics) {
209
+ return diagnostics.map(formatDiagnostic);
210
+ }
211
+ /** The contract's deterministic order: by pointer, then by message, ASCII. */
212
+ export function sortDiagnostics(diagnostics) {
213
+ const sorted = [...diagnostics];
214
+ sorted.sort((a, b) => {
215
+ if (a.pointer !== b.pointer) {
216
+ return a.pointer < b.pointer ? -1 : 1;
217
+ }
218
+ if (a.message === b.message) {
219
+ return 0;
220
+ }
221
+ return a.message < b.message ? -1 : 1;
222
+ });
223
+ return sorted;
224
+ }
225
+ function escapeSegment(segment) {
226
+ return segment.replace(/~/g, "~0").replace(/\//g, "~1");
227
+ }
228
+ /** Ajv's `instancePath` is a bare pointer; the contract's is fragment form. */
229
+ function toFragmentPointer(instancePath, extra) {
230
+ const base = instancePath === "" ? ROOT_POINTER : `${ROOT_POINTER}${instancePath}`;
231
+ return extra === undefined ? base : `${base}/${escapeSegment(extra)}`;
232
+ }
233
+ /** Resolve an Ajv `instancePath` against the instance, for reporting values. */
234
+ function valueAt(instance, instancePath) {
235
+ if (instancePath === "") {
236
+ return instance;
237
+ }
238
+ let node = instance;
239
+ for (const raw of instancePath.slice(1).split("/")) {
240
+ const segment = raw.replace(/~1/g, "/").replace(/~0/g, "~");
241
+ if (Array.isArray(node)) {
242
+ node = node[Number(segment)];
243
+ continue;
244
+ }
245
+ if (typeof node === "object" && node !== null) {
246
+ node = Object.prototype.hasOwnProperty.call(node, segment)
247
+ ? node[segment]
248
+ : undefined;
249
+ continue;
250
+ }
251
+ return undefined;
252
+ }
253
+ return node;
254
+ }
255
+ /**
256
+ * Ajv keywords this engine translates. A keyword absent from this map is a
257
+ * defect and says so: see the module header. The map's KEY SET is also the
258
+ * mechanical half of "every keyword in the declared vocabulary has a
259
+ * diagnostic", asserted in `test/schemas.test.ts`.
260
+ */
261
+ const MESSAGE_BY_KEYWORD = new Set([
262
+ "type",
263
+ "required",
264
+ "additionalProperties",
265
+ "enum",
266
+ "const",
267
+ "minimum",
268
+ "maximum",
269
+ "minItems",
270
+ "maxItems",
271
+ "minLength",
272
+ "pattern",
273
+ "uniqueItems",
274
+ "contains",
275
+ "oneOf",
276
+ "if",
277
+ ]);
278
+ /**
279
+ * Translate ONE Ajv error into the Tiphys contract. `undefined` means the
280
+ * error carries no independent information for a reader (Ajv reports a
281
+ * failing branch of `oneOf`/`if` as well as the composite), and the caller
282
+ * drops it.
283
+ */
284
+ function renderAjvError(error, instance,
285
+ /**
286
+ * `contains` subschema path -> the `const` that subschema requires, for the
287
+ * errors where it is a bare `const`. Built by the caller from the SUBSIDIARY
288
+ * errors Ajv reports beside the composite, which `isSubsidiary` then drops:
289
+ * the value is already in the error stream and was being thrown away.
290
+ */
291
+ containsConst = new Map()) {
292
+ const params = error.params;
293
+ const at = error.instancePath;
294
+ switch (error.keyword) {
295
+ case "type":
296
+ return {
297
+ pointer: toFragmentPointer(at),
298
+ message: DIAGNOSTIC_MESSAGES.type(String(params["type"]), jsonTypeOf(valueAt(instance, at))),
299
+ };
300
+ case "required":
301
+ return {
302
+ pointer: toFragmentPointer(at, String(params["missingProperty"])),
303
+ message: DIAGNOSTIC_MESSAGES.required(String(params["missingProperty"])),
304
+ };
305
+ case "additionalProperties":
306
+ return {
307
+ pointer: toFragmentPointer(at, String(params["additionalProperty"])),
308
+ message: DIAGNOSTIC_MESSAGES.additionalProperties(String(params["additionalProperty"])),
309
+ };
310
+ case "enum":
311
+ return {
312
+ pointer: toFragmentPointer(at),
313
+ message: DIAGNOSTIC_MESSAGES.enum(render(valueAt(instance, at)), params["allowedValues"].map((v) => render(v)).join(", ")),
314
+ };
315
+ case "const":
316
+ return {
317
+ pointer: toFragmentPointer(at),
318
+ message: DIAGNOSTIC_MESSAGES.const(render(valueAt(instance, at)), render(params["allowedValue"])),
319
+ };
320
+ case "minimum":
321
+ return {
322
+ pointer: toFragmentPointer(at),
323
+ message: DIAGNOSTIC_MESSAGES.minimum(render(valueAt(instance, at)), render(params["limit"])),
324
+ };
325
+ case "maximum":
326
+ return {
327
+ pointer: toFragmentPointer(at),
328
+ message: DIAGNOSTIC_MESSAGES.maximum(render(valueAt(instance, at)), render(params["limit"])),
329
+ };
330
+ case "minItems": {
331
+ const value = valueAt(instance, at);
332
+ return {
333
+ pointer: toFragmentPointer(at),
334
+ message: DIAGNOSTIC_MESSAGES.minItems(String(Array.isArray(value) ? value.length : 0), String(params["limit"])),
335
+ };
336
+ }
337
+ case "maxItems": {
338
+ const value = valueAt(instance, at);
339
+ return {
340
+ pointer: toFragmentPointer(at),
341
+ message: DIAGNOSTIC_MESSAGES.maxItems(String(Array.isArray(value) ? value.length : 0), String(params["limit"])),
342
+ };
343
+ }
344
+ case "minLength":
345
+ return {
346
+ pointer: toFragmentPointer(at),
347
+ message: DIAGNOSTIC_MESSAGES.minLength(render(valueAt(instance, at)), String(params["limit"])),
348
+ };
349
+ case "pattern":
350
+ return {
351
+ pointer: toFragmentPointer(at),
352
+ message: DIAGNOSTIC_MESSAGES.pattern(render(valueAt(instance, at)), String(params["pattern"])),
353
+ };
354
+ case "uniqueItems":
355
+ return {
356
+ pointer: toFragmentPointer(at),
357
+ /* Ajv reports the LATER index first. The contract sorts them, so
358
+ the line reads in document order and does not depend on which
359
+ direction the engine happened to compare in. */
360
+ message: DIAGNOSTIC_MESSAGES.uniqueItems(String(Math.min(Number(params["i"]), Number(params["j"]))), String(Math.max(Number(params["i"]), Number(params["j"])))),
361
+ };
362
+ case "contains": {
363
+ const minimum = String(params["minContains"] ?? 1);
364
+ const required = containsConst.get(error.schemaPath);
365
+ return {
366
+ pointer: toFragmentPointer(at),
367
+ message: required === undefined
368
+ ? DIAGNOSTIC_MESSAGES.contains(minimum)
369
+ : DIAGNOSTIC_MESSAGES.containsValue(render(required), minimum),
370
+ };
371
+ }
372
+ case "oneOf":
373
+ /* `anyOf`, `allOf` and `not` are not in the AUTHORING vocabulary, so no
374
+ Tiphys schema uses them. They still reach this renderer, because
375
+ META-SCHEMA validation runs the 2020-12 meta-schema (which uses all
376
+ three) over a Tiphys schema document. Mapping them here is what stopped
377
+ `internal defect: no Tiphys diagnostic is defined for schema keyword
378
+ anyOf` from being the diagnostic a bad `type` produced, which is how
379
+ the untranslated-keyword guard earned its keep. */
380
+ case "anyOf":
381
+ case "allOf":
382
+ case "not":
383
+ return {
384
+ pointer: toFragmentPointer(at),
385
+ message: DIAGNOSTIC_MESSAGES.oneOf(),
386
+ };
387
+ case "if":
388
+ return {
389
+ pointer: toFragmentPointer(at),
390
+ message: DIAGNOSTIC_MESSAGES.ifThen(),
391
+ };
392
+ default:
393
+ return {
394
+ pointer: toFragmentPointer(at),
395
+ message: DIAGNOSTIC_MESSAGES.untranslated(error.keyword),
396
+ };
397
+ }
398
+ }
399
+ /**
400
+ * Is this Ajv error a SUBSIDIARY of a composite the caller already reports?
401
+ *
402
+ * Ajv with allErrors reports the failing branches inside
403
+ * `oneOf`/`anyOf`/`not`/`contains` as well as the composite keyword itself.
404
+ * Which branch is reported, and how many, depends on Ajv's branch ordering and
405
+ * on the instance's length, which is exactly the nondeterminism the contract
406
+ * forbids, so only the COMPOSITE line survives: its pointer is the instance
407
+ * location the author must look at. `contains` is in this list for the second
408
+ * reason rather than the first: a ten-item array that contains nothing
409
+ * matching would otherwise emit eleven lines for one fault.
410
+ *
411
+ * `if`/`then`/`else` is DELIBERATELY NOT in that list. Exactly one branch
412
+ * applies and which one is decided by the instance, not by the engine, so the
413
+ * branch's own diagnostics are deterministic AND are the informative ones: a
414
+ * charter with `mode: none` and no `reason` must say `reason`, which only the
415
+ * `then` branch's `required` error carries (criterion 5b).
416
+ */
417
+ function isSubsidiary(error) {
418
+ return /\/(oneOf|anyOf|not|contains)\//.test(error.schemaPath);
419
+ }
420
+ /**
421
+ * A fresh Ajv, configured EXACTLY as DR-0013 clause 4 lists. Each policy is
422
+ * named so a diff that removes one is visible as the removal of a decided
423
+ * policy rather than as a formatting change.
424
+ */
425
+ export function makeAjv() {
426
+ const { Ajv2020: Constructor } = ajvModule();
427
+ return new Constructor({
428
+ strict: true,
429
+ allErrors: true,
430
+ validateSchema: true,
431
+ coerceTypes: false,
432
+ useDefaults: false,
433
+ removeAdditional: false,
434
+ /* No `loadSchema`: an unresolved remote reference fails compilation
435
+ rather than being fetched (DR-0013 clause 4, criterion 7). */
436
+ });
437
+ }
438
+ /**
439
+ * COMPANION SCHEMAS (M3-P4). A shipped schema may `$ref` a definition in a
440
+ * SIBLING shipped schema, so that a contract shared by two artifact types has
441
+ * ONE definition rather than two that can drift. `schemas/work-history.schema.json`
442
+ * is the first instance: its `claims[]`, `fix-round[]` and `gate-evidence[]`
443
+ * items resolve into `schemas/report.schema.json`.
444
+ *
445
+ * THIS IS NOT THE REMOTE LOADING DR-0013 CLAUSE 4 WITHHOLDS, and the
446
+ * difference is the whole reason the parameter exists rather than a
447
+ * `loadSchema` callback. A companion is supplied BY THE CALLER, from the
448
+ * shipped `schemas/` directory, named in `src/commands/validate.ts`'s
449
+ * companion table beside the type table. Nothing is fetched, nothing is
450
+ * resolved from the reference itself, and a `$ref` naming a document the
451
+ * caller did not supply still fails COMPILATION with the existing
452
+ * `unresolvedRef` or `remoteRef` diagnostic. The set of documents a schema
453
+ * may reach is therefore declared in one auditable place, which is the
454
+ * property clause 4 is protecting.
455
+ */
456
+ const compiled = new WeakMap();
457
+ function sameCompanions(left, right) {
458
+ return (left.length === right.length && left.every((one, index) => one === right[index]));
459
+ }
460
+ /**
461
+ * Compile a schema. Cached by schema OBJECT IDENTITY, because a fresh Ajv per
462
+ * call is both slow and wrong: two schemas carrying the same `$id` cannot
463
+ * share one Ajv instance, so each schema gets its own.
464
+ *
465
+ * IDENTITY, NOT CONTENT, and it has one observable consequence worth stating
466
+ * rather than discovering. A caller that MUTATES a schema object in place
467
+ * after compiling it keeps the old validator, because the WeakMap key is
468
+ * unchanged. That is correct for how schemas are used here (documents read
469
+ * from disk and never edited), and it was measured: the first attempt at
470
+ * criterion 4's red witness defanged a keyword in place and the diagnostics
471
+ * did not move, which read exactly like a schema whose keyword was doing
472
+ * nothing. The witness was redone with a schema re-read from disk per arm.
473
+ * Any future caller that wants to compile a modified schema must hand over a
474
+ * NEW object.
475
+ *
476
+ * The cache is keyed by the schema object AND by the COMPANION LIST, also by
477
+ * identity: the same document compiled with and without a companion is two
478
+ * different compilations, and returning the first for the second would be the
479
+ * quiet wrong answer. Companion lists here are one or zero long, so the scan
480
+ * is a scan of a one-element array.
481
+ */
482
+ export function compileSchema(schema, companions = []) {
483
+ const entries = compiled.get(schema) ?? [];
484
+ const cached = entries.find((entry) => sameCompanions(entry.companions, companions));
485
+ if (cached !== undefined) {
486
+ return cached.result;
487
+ }
488
+ let result;
489
+ const ajv = makeAjv();
490
+ try {
491
+ for (const companion of companions) {
492
+ ajv.addSchema(companion);
493
+ }
494
+ result = { ok: true, validator: ajv.compile(schema) };
495
+ }
496
+ catch (error) {
497
+ const diagnostics = compilationDiagnostics(error, ajv, schema);
498
+ result = {
499
+ ok: false,
500
+ diagnostics,
501
+ reason: formatDiagnostics(diagnostics).join("; "),
502
+ };
503
+ }
504
+ entries.push({ companions: [...companions], result });
505
+ compiled.set(schema, entries);
506
+ return result;
507
+ }
508
+ function ajvText(error) {
509
+ const text = error instanceof Error ? error.message : String(error);
510
+ return text.replace(/\s+/g, " ").trim();
511
+ }
512
+ /**
513
+ * Translate a COMPILATION failure into the Tiphys diagnostic contract.
514
+ *
515
+ * The classification reads Ajv's exception only to EXTRACT IDENTIFIERS from
516
+ * it; nothing Ajv wrote is passed through. An unrecognised shape becomes the
517
+ * bare `schema could not be compiled`, which loses detail deliberately:
518
+ * losing detail is recoverable, and leaking a third party's wording into a
519
+ * contract nine phases are written against is not.
520
+ */
521
+ export function compilationDiagnostics(error, ajv, schema) {
522
+ const text = ajvText(error);
523
+ const unknownKeyword = /unknown keyword: "([^"]+)"/.exec(text);
524
+ if (unknownKeyword !== null) {
525
+ return [
526
+ {
527
+ pointer: ROOT_POINTER,
528
+ message: DIAGNOSTIC_MESSAGES.unknownKeyword(unknownKeyword[1]),
529
+ },
530
+ ];
531
+ }
532
+ const untyped = /missing type "([^"]+)" for keyword "([^"]+)" at "([^"]+)"/.exec(text);
533
+ if (untyped !== null) {
534
+ return [
535
+ {
536
+ pointer: untyped[3].startsWith("#")
537
+ ? untyped[3]
538
+ : ROOT_POINTER,
539
+ message: DIAGNOSTIC_MESSAGES.strictPolicyUntyped(untyped[2], untyped[1]),
540
+ },
541
+ ];
542
+ }
543
+ const missingRef = /can't resolve reference (\S+) from id/.exec(text);
544
+ if (missingRef !== null) {
545
+ const reference = missingRef[1];
546
+ /* A scheme-qualified reference is REMOTE, and DR-0013 clause 4 withholds
547
+ the loader that would fetch it, so it fails closed rather than being
548
+ skipped or retrieved. A bare `#/...` that does not resolve is the local
549
+ case and gets the local message. */
550
+ const remote = /^[a-z][a-z0-9+.-]*:/i.test(reference);
551
+ return [
552
+ {
553
+ pointer: ROOT_POINTER,
554
+ message: remote
555
+ ? DIAGNOSTIC_MESSAGES.remoteRef(reference)
556
+ : DIAGNOSTIC_MESSAGES.unresolvedRef(reference),
557
+ },
558
+ ];
559
+ }
560
+ if (text.startsWith("schema is invalid")) {
561
+ /* META-SCHEMA VALIDATION. Ajv leaves the failures on the instance, and
562
+ they are ordinary errors over the SCHEMA as the instance, so they route
563
+ through the same renderer and come out with pointers INTO the schema.
564
+ That is strictly more useful than Ajv's sentence and owes it nothing. */
565
+ const metaErrors = ajv.errors ?? [];
566
+ const mapped = metaErrors
567
+ .filter((one) => !isSubsidiary(one))
568
+ .map((one) => renderAjvError(one, schema))
569
+ .filter((one) => one !== undefined);
570
+ return sortDiagnostics([
571
+ { pointer: ROOT_POINTER, message: DIAGNOSTIC_MESSAGES.invalidSchemaDocument() },
572
+ ...mapped,
573
+ ]);
574
+ }
575
+ if (error instanceof SyntaxError && /regular expression/i.test(text)) {
576
+ return [
577
+ { pointer: ROOT_POINTER, message: DIAGNOSTIC_MESSAGES.patternUncompilable() },
578
+ ];
579
+ }
580
+ if (error instanceof RangeError) {
581
+ return [
582
+ { pointer: ROOT_POINTER, message: DIAGNOSTIC_MESSAGES.cyclicCompilation() },
583
+ ];
584
+ }
585
+ if (text.startsWith("strict mode")) {
586
+ return [{ pointer: ROOT_POINTER, message: DIAGNOSTIC_MESSAGES.strictPolicy() }];
587
+ }
588
+ return [{ pointer: ROOT_POINTER, message: DIAGNOSTIC_MESSAGES.uncompilable() }];
589
+ }
590
+ /**
591
+ * Validate an already-decoded instance against a schema document.
592
+ *
593
+ * Returns diagnostics in the contract's deterministic order. A schema that
594
+ * cannot be compiled yields ONE root diagnostic rather than a throw: this
595
+ * function is called from a CLI whose ordinary input is hand-authored, and a
596
+ * thrown compile error there is the stack trace step 8b exists to prevent.
597
+ */
598
+ export function validateInstance(schema, instance, companions = []) {
599
+ const compilation = compileSchema(schema, companions);
600
+ if (!compilation.ok) {
601
+ return compilation.diagnostics;
602
+ }
603
+ const ok = compilation.validator(instance);
604
+ if (ok) {
605
+ return [];
606
+ }
607
+ const errors = compilation.validator.errors ?? [];
608
+ /* CR-004. Ajv reports, beside every failing `contains`, one subsidiary
609
+ `const` error per array item carrying the required value in
610
+ `params.allowedValue`. `isSubsidiary` drops those (they are one fault
611
+ reported N times), so the value was being discarded a few lines before the
612
+ message that needed it. Harvest it first, keyed by the contains
613
+ subschema's own schemaPath, which is the subsidiary's path minus the
614
+ trailing `/const`. */
615
+ const containsConst = new Map();
616
+ for (const error of errors) {
617
+ if (error.keyword === "const" && error.schemaPath.endsWith("/contains/const")) {
618
+ containsConst.set(error.schemaPath.slice(0, -"/const".length), error.params["allowedValue"]);
619
+ }
620
+ }
621
+ const diagnostics = [];
622
+ for (const error of errors) {
623
+ if (isSubsidiary(error)) {
624
+ continue;
625
+ }
626
+ const diagnostic = renderAjvError(error, instance, containsConst);
627
+ if (diagnostic !== undefined) {
628
+ diagnostics.push(diagnostic);
629
+ }
630
+ }
631
+ /* Ajv can report the same pointer and message twice through two branches;
632
+ the contract emits one line per distinct fault. */
633
+ const unique = new Map();
634
+ for (const diagnostic of diagnostics) {
635
+ unique.set(formatDiagnostic(diagnostic), diagnostic);
636
+ }
637
+ return sortDiagnostics([...unique.values()]);
638
+ }
639
+ /** Validate and format in one step. */
640
+ export function validateToLines(schema, instance, companions = []) {
641
+ return formatDiagnostics(validateInstance(schema, instance, companions));
642
+ }
643
+ /**
644
+ * Decode YAML (or JSON, which YAML 1.2 is a superset of) into a plain value.
645
+ *
646
+ * A parse failure returns a reason, never a throw and never a stack trace.
647
+ * `yaml` reports position information; it is collapsed to one line because
648
+ * the contract is one diagnostic line per failure.
649
+ */
650
+ export function decodeDocument(text, label) {
651
+ const { parse: parseYaml, YAMLParseError } = yamlModule();
652
+ let value;
653
+ try {
654
+ value = parseYaml(text, { prettyErrors: false });
655
+ }
656
+ catch (error) {
657
+ if (error instanceof YAMLParseError) {
658
+ const where = error.linePos?.[0];
659
+ const at = where === undefined ? "" : ` at line ${String(where.line)} column ${String(where.col)}`;
660
+ return {
661
+ ok: false,
662
+ reason: `${label} is not valid YAML${at}: ${ajvText(error.message)}`,
663
+ };
664
+ }
665
+ return { ok: false, reason: `${label} could not be decoded: ${ajvText(error)}` };
666
+ }
667
+ return { ok: true, value };
668
+ }
669
+ /**
670
+ * READ A PATH THIS PROGRAM DID NOT CREATE (D-M3-27).
671
+ *
672
+ * `tiphys validate <file>` takes an operator-supplied path, so the path may
673
+ * be a FIFO, a directory, a socket or a dangling symlink. Opening a FIFO with
674
+ * no writer blocks forever and the command never returns, which is the M1-P5
675
+ * class (CR-520). The type is established with `classifyEntry` BEFORE the
676
+ * open, and an irregular entry is refused naming the path and the observed
677
+ * type.
678
+ */
679
+ export function readOperatorPath(path) {
680
+ const entry = classifyEntry(path);
681
+ if (entry.kind === "absent" || entry.kind === "dangling") {
682
+ return { ok: false, reason: `${path} does not exist` };
683
+ }
684
+ if (entry.kind === "irregular" || entry.kind === "unexaminable") {
685
+ return { ok: false, reason: entry.reason };
686
+ }
687
+ try {
688
+ return { ok: true, body: readFileSync(path, "utf8") };
689
+ }
690
+ catch (error) {
691
+ return { ok: false, reason: `${path} could not be read: ${ajvText(error)}` };
692
+ }
693
+ }
694
+ /**
695
+ * Establish that a path is a DIRECTORY before walking it. Same hazard, same
696
+ * rule: `--context` is operator-supplied and a FIFO there would block the
697
+ * walk exactly as it blocks a read.
698
+ */
699
+ export function classifyContextDirectory(path) {
700
+ const entry = classifyEntry(path);
701
+ if (entry.kind === "absent" || entry.kind === "dangling") {
702
+ return `${path} does not exist`;
703
+ }
704
+ if (entry.kind === "unexaminable") {
705
+ return entry.reason;
706
+ }
707
+ if (entry.kind === "regular") {
708
+ return `${path} is a regular file, not a directory, so it was not walked`;
709
+ }
710
+ /* `classifyEntry` calls everything that is not a regular file "irregular",
711
+ including a directory, which is what a context path is SUPPOSED to be.
712
+ Distinguish here rather than widening classifyEntry, whose callers all
713
+ want a regular file (M2's fleet-state reads). */
714
+ const stats = safeStat(path);
715
+ if (stats === undefined) {
716
+ return `${path} could not be examined`;
717
+ }
718
+ if (!stats.isDirectory()) {
719
+ return entry.kind === "irregular"
720
+ ? entry.reason.replace("not a regular file", "not a directory")
721
+ : `${path} is not a directory`;
722
+ }
723
+ return undefined;
724
+ }
725
+ function safeStat(path) {
726
+ try {
727
+ return statSync(path);
728
+ }
729
+ catch {
730
+ return undefined;
731
+ }
732
+ }