@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.
- package/AGENTS.md +611 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +278 -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 +14 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +59 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +828 -0
- package/dist/src/checks.js +3314 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +57 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +320 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/doctor.d.ts +35 -0
- package/dist/src/commands/doctor.js +605 -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 +128 -0
- package/dist/src/commands/lock.d.ts +1 -0
- package/dist/src/commands/lock.js +229 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -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 +128 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +146 -0
- package/dist/src/commands/status.d.ts +19 -0
- package/dist/src/commands/status.js +120 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +79 -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 +360 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/exec/env.d.ts +128 -0
- package/dist/src/exec/env.js +190 -0
- package/dist/src/fleet.d.ts +51 -0
- package/dist/src/fleet.js +80 -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 +1163 -0
- package/dist/src/gates/coverage.d.ts +284 -0
- package/dist/src/gates/coverage.js +701 -0
- package/dist/src/gates/credentials.d.ts +74 -0
- package/dist/src/gates/credentials.js +533 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -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 +390 -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 +566 -0
- package/dist/src/gates/run.js +1536 -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 +42 -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 +927 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +32 -0
- package/dist/src/hooks.js +62 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +178 -0
- package/dist/src/lock.js +500 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +2 -0
- package/dist/src/path-identity.js +10 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +130 -0
- package/dist/src/pool.js +721 -0
- package/dist/src/roles.d.ts +430 -0
- package/dist/src/roles.js +734 -0
- package/dist/src/spawn.d.ts +177 -0
- package/dist/src/spawn.js +332 -0
- package/dist/src/status.d.ts +91 -0
- package/dist/src/status.js +119 -0
- package/dist/src/task.d.ts +264 -0
- package/dist/src/task.js +305 -0
- package/dist/src/teardown.d.ts +32 -0
- package/dist/src/teardown.js +314 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +230 -0
- package/dist/src/validate.js +732 -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 +859 -0
- package/dist/src/witness/run.d.ts +274 -0
- package/dist/src/witness/run.js +1327 -0
- package/dist/src/witness/spec.d.ts +102 -0
- package/dist/src/witness/spec.js +253 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +390 -0
- package/gates.manifest.json +195 -0
- package/package.json +57 -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 +460 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +81 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +166 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/decision-record.schema.json +88 -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/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 +289 -0
- package/schemas/work-history.schema.json +183 -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,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
|
+
}
|