@tiphys/kernel 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/AGENTS.md +663 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +299 -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 +92 -0
  11. package/dist/src/adapters/load.d.ts +202 -0
  12. package/dist/src/adapters/load.js +440 -0
  13. package/dist/src/brief.d.ts +32 -0
  14. package/dist/src/brief.js +66 -0
  15. package/dist/src/checklists.d.ts +169 -0
  16. package/dist/src/checklists.js +310 -0
  17. package/dist/src/checks.d.ts +1539 -0
  18. package/dist/src/checks.js +5025 -0
  19. package/dist/src/cli.d.ts +9 -0
  20. package/dist/src/cli.js +68 -0
  21. package/dist/src/commands/brief.d.ts +92 -0
  22. package/dist/src/commands/brief.js +343 -0
  23. package/dist/src/commands/checklist.d.ts +42 -0
  24. package/dist/src/commands/checklist.js +168 -0
  25. package/dist/src/commands/cutover.d.ts +35 -0
  26. package/dist/src/commands/cutover.js +448 -0
  27. package/dist/src/commands/doctor.d.ts +264 -0
  28. package/dist/src/commands/doctor.js +1546 -0
  29. package/dist/src/commands/gates.d.ts +9 -0
  30. package/dist/src/commands/gates.js +360 -0
  31. package/dist/src/commands/init.d.ts +30 -0
  32. package/dist/src/commands/init.js +177 -0
  33. package/dist/src/commands/lock.d.ts +34 -0
  34. package/dist/src/commands/lock.js +340 -0
  35. package/dist/src/commands/mode.d.ts +40 -0
  36. package/dist/src/commands/mode.js +134 -0
  37. package/dist/src/commands/next.d.ts +130 -0
  38. package/dist/src/commands/next.js +597 -0
  39. package/dist/src/commands/plan.d.ts +20 -0
  40. package/dist/src/commands/plan.js +105 -0
  41. package/dist/src/commands/pool.d.ts +1 -0
  42. package/dist/src/commands/pool.js +139 -0
  43. package/dist/src/commands/resume.d.ts +1 -0
  44. package/dist/src/commands/resume.js +88 -0
  45. package/dist/src/commands/spawn.d.ts +1 -0
  46. package/dist/src/commands/spawn.js +195 -0
  47. package/dist/src/commands/status.d.ts +21 -0
  48. package/dist/src/commands/status.js +122 -0
  49. package/dist/src/commands/sync.d.ts +47 -0
  50. package/dist/src/commands/sync.js +341 -0
  51. package/dist/src/commands/teardown.d.ts +1 -0
  52. package/dist/src/commands/teardown.js +87 -0
  53. package/dist/src/commands/tuition.d.ts +21 -0
  54. package/dist/src/commands/tuition.js +218 -0
  55. package/dist/src/commands/validate.d.ts +78 -0
  56. package/dist/src/commands/validate.js +430 -0
  57. package/dist/src/commands/watch.d.ts +1 -0
  58. package/dist/src/commands/watch.js +172 -0
  59. package/dist/src/cutover.d.ts +584 -0
  60. package/dist/src/cutover.js +1444 -0
  61. package/dist/src/exclusion.d.ts +389 -0
  62. package/dist/src/exclusion.js +843 -0
  63. package/dist/src/exec/env.d.ts +278 -0
  64. package/dist/src/exec/env.js +334 -0
  65. package/dist/src/fleet.d.ts +223 -0
  66. package/dist/src/fleet.js +298 -0
  67. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  68. package/dist/src/gates/adapters/http-json.js +283 -0
  69. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  70. package/dist/src/gates/adapters/migrations-command.js +373 -0
  71. package/dist/src/gates/citations.d.ts +408 -0
  72. package/dist/src/gates/citations.js +1169 -0
  73. package/dist/src/gates/coverage.d.ts +375 -0
  74. package/dist/src/gates/coverage.js +836 -0
  75. package/dist/src/gates/credentials.d.ts +233 -0
  76. package/dist/src/gates/credentials.js +752 -0
  77. package/dist/src/gates/deploy.d.ts +1 -0
  78. package/dist/src/gates/deploy.js +33 -0
  79. package/dist/src/gates/gate-classes.d.ts +56 -0
  80. package/dist/src/gates/gate-classes.js +633 -0
  81. package/dist/src/gates/manifest.d.ts +99 -0
  82. package/dist/src/gates/manifest.js +208 -0
  83. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  84. package/dist/src/gates/merge-preconditions.js +932 -0
  85. package/dist/src/gates/migrations.d.ts +1 -0
  86. package/dist/src/gates/migrations.js +36 -0
  87. package/dist/src/gates/pin.d.ts +114 -0
  88. package/dist/src/gates/pin.js +154 -0
  89. package/dist/src/gates/red-witness.d.ts +22 -0
  90. package/dist/src/gates/red-witness.js +482 -0
  91. package/dist/src/gates/release.d.ts +283 -0
  92. package/dist/src/gates/release.js +820 -0
  93. package/dist/src/gates/result.d.ts +116 -0
  94. package/dist/src/gates/result.js +91 -0
  95. package/dist/src/gates/run.d.ts +614 -0
  96. package/dist/src/gates/run.js +1614 -0
  97. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  98. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  99. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  100. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  101. package/dist/src/gates/schemas/phase-declaration.schema.json +87 -0
  102. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  103. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  104. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  105. package/dist/src/gates/scope.d.ts +131 -0
  106. package/dist/src/gates/scope.js +1018 -0
  107. package/dist/src/gates/suite.d.ts +217 -0
  108. package/dist/src/gates/suite.js +968 -0
  109. package/dist/src/gates/validate.d.ts +121 -0
  110. package/dist/src/gates/validate.js +414 -0
  111. package/dist/src/hooks.d.ts +84 -0
  112. package/dist/src/hooks.js +125 -0
  113. package/dist/src/index.d.ts +31 -0
  114. package/dist/src/index.js +30 -0
  115. package/dist/src/liveness.d.ts +321 -0
  116. package/dist/src/liveness.js +396 -0
  117. package/dist/src/lock.d.ts +256 -0
  118. package/dist/src/lock.js +792 -0
  119. package/dist/src/model-resolution.d.ts +159 -0
  120. package/dist/src/model-resolution.js +307 -0
  121. package/dist/src/modes.d.ts +149 -0
  122. package/dist/src/modes.js +258 -0
  123. package/dist/src/path-identity.d.ts +34 -0
  124. package/dist/src/path-identity.js +48 -0
  125. package/dist/src/plan.d.ts +73 -0
  126. package/dist/src/plan.js +153 -0
  127. package/dist/src/pool.d.ts +326 -0
  128. package/dist/src/pool.js +988 -0
  129. package/dist/src/roles.d.ts +461 -0
  130. package/dist/src/roles.js +776 -0
  131. package/dist/src/spawn.d.ts +482 -0
  132. package/dist/src/spawn.js +1003 -0
  133. package/dist/src/status.d.ts +116 -0
  134. package/dist/src/status.js +148 -0
  135. package/dist/src/task.d.ts +504 -0
  136. package/dist/src/task.js +307 -0
  137. package/dist/src/teardown.d.ts +39 -0
  138. package/dist/src/teardown.js +422 -0
  139. package/dist/src/tuition.d.ts +159 -0
  140. package/dist/src/tuition.js +311 -0
  141. package/dist/src/validate.d.ts +263 -0
  142. package/dist/src/validate.js +742 -0
  143. package/dist/src/version.d.ts +3 -0
  144. package/dist/src/version.js +38 -0
  145. package/dist/src/watcher.d.ts +275 -0
  146. package/dist/src/watcher.js +849 -0
  147. package/dist/src/witness/run.d.ts +299 -0
  148. package/dist/src/witness/run.js +1373 -0
  149. package/dist/src/witness/spec.d.ts +270 -0
  150. package/dist/src/witness/spec.js +475 -0
  151. package/dist/tsconfig.src.tsbuildinfo +1 -0
  152. package/gate-registry.yaml +526 -0
  153. package/gates.manifest.json +257 -0
  154. package/package.json +72 -3
  155. package/role-model-config.yaml +88 -0
  156. package/roles/README.md +128 -0
  157. package/roles/_shared-dispatch-contract.md +87 -0
  158. package/roles/adversarial-plan-reviewer.md +80 -0
  159. package/roles/clean-room-reviewer.md +140 -0
  160. package/roles/implementer.md +463 -0
  161. package/roles/investigator.md +138 -0
  162. package/roles/plan-writer.md +95 -0
  163. package/schemas/README.md +82 -0
  164. package/schemas/assurance-modes.schema.json +264 -0
  165. package/schemas/charter.schema.json +185 -0
  166. package/schemas/checklist.schema.json +114 -0
  167. package/schemas/cutover-state.schema.json +64 -0
  168. package/schemas/decision-record.schema.json +88 -0
  169. package/schemas/executor-record.schema.json +36 -0
  170. package/schemas/final-report.schema.json +90 -0
  171. package/schemas/finding.schema.json +106 -0
  172. package/schemas/gate-registry.schema.json +260 -0
  173. package/schemas/mechanism-index.schema.json +94 -0
  174. package/schemas/model-resolution.schema.json +362 -0
  175. package/schemas/plan.schema.json +300 -0
  176. package/schemas/report.schema.json +579 -0
  177. package/schemas/role-brief.schema.json +105 -0
  178. package/schemas/role-model-config.schema.json +90 -0
  179. package/schemas/status-line.schema.json +40 -0
  180. package/schemas/tuition.schema.json +191 -0
  181. package/schemas/verdict.schema.json +295 -0
  182. package/schemas/work-history.schema.json +183 -0
  183. package/schemas/write-bypass.schema.json +69 -0
  184. package/templates/charter.example.yaml +54 -0
  185. package/templates/decision-record.example.yaml +27 -0
  186. package/templates/final-report.example.yaml +80 -0
  187. package/templates/plan.example.yaml +87 -0
  188. package/templates/report.example.yaml +236 -0
  189. package/templates/warnings.md +74 -0
  190. package/templates/work-history.example.yaml +185 -0
  191. package/tuition/README.md +76 -0
  192. package/tuition/T-001.yaml +48 -0
  193. package/tuition/T-002.yaml +51 -0
  194. package/tuition/T-003.yaml +100 -0
  195. package/tuition/T-004.yaml +52 -0
  196. package/tuition/T-005.yaml +72 -0
  197. package/tuition/T-006.yaml +81 -0
  198. package/tuition/T-007.yaml +56 -0
  199. package/tuition/T-008.yaml +111 -0
  200. package/tuition/T-009.yaml +50 -0
  201. package/tuition/T-015.yaml +36 -0
  202. package/tuition/T-016.yaml +36 -0
  203. package/tuition/T-017.yaml +46 -0
  204. package/tuition/T-018.yaml +84 -0
  205. package/tuition/T-021.yaml +40 -0
  206. package/tuition/T-022.yaml +36 -0
  207. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * THE MINIMAL IN-REPO SCHEMA VALIDATOR (kernel plan M2, M2-P1 step 4;
3
+ * M2-D-04; DR-0013 clause 6).
4
+ *
5
+ * M1 shipped zero runtime dependencies. DR-0013 decided the library
6
+ * question for M3 (Ajv 8.20.0 exact, Draft 2020-12) and made THIS module the
7
+ * seam: M3-P1 retires the engine below and keeps the boundary. That promise
8
+ * is only keepable if two things are true here, and both are enforced by
9
+ * this module's tests rather than asserted in prose:
10
+ *
11
+ * 1. THE DIAGNOSTIC CONTRACT IS `INVALID <json-pointer> <message>`, one
12
+ * line per failure, in a deterministic order. That string is this
13
+ * module's public output. Everything else about how a failure was
14
+ * found is an implementation detail and no test may assert on it.
15
+ * 2. THE MESSAGE TEXTS BELOW ARE THE CONTRACT, not this engine's wording.
16
+ * They are listed in `DIAGNOSTIC_MESSAGES` so a future engine has one
17
+ * table to map onto instead of a search through call sites.
18
+ *
19
+ * Without those two, DR-0013's "all existing M2 validation tests are re-run
20
+ * against it" would be a rewrite of the tests rather than an engine swap,
21
+ * and "the module boundary is preserved" would be false at the moment it was
22
+ * acted on.
23
+ *
24
+ * DETERMINISTIC ORDERING, stated so a replacement engine can reproduce it:
25
+ * every diagnostic is collected during a traversal that visits object
26
+ * properties in sorted key order and array items in index order, and the
27
+ * collected list is then sorted by (pointer, message) with ASCII
28
+ * lexicographic comparison. The final sort is what makes the order a
29
+ * property of the CONTRACT rather than of the traversal, so an engine that
30
+ * walks differently still emits the same lines in the same order.
31
+ *
32
+ * THE CLOSED KEYWORD SET, AND WHY IT IS LOUD (M2-D-04). Ten validation
33
+ * keywords plus local `$ref`. A schema document containing any other
34
+ * keyword is a LOAD ERROR naming the keyword and its location. It is never
35
+ * ignored, because a validator that silently skips `oneOf` reports a
36
+ * document valid while never having checked the constraint that mattered,
37
+ * which is the same shape as "green by omission" one layer down. The set is
38
+ * deliberately NOT grown to anticipate M3's five artifact schemas; DR-0013
39
+ * decided that question against extension.
40
+ */
41
+ /** The ten validation keywords plus local `$ref`. Nothing else validates. */
42
+ export declare const VALIDATION_KEYWORDS: readonly string[];
43
+ /**
44
+ * Keys that carry no constraint and are permitted for documentation and for
45
+ * holding subschemas. They are listed separately from the validation
46
+ * keywords on purpose: a reader must be able to see that permitting
47
+ * `description` is a decision about annotations and not a hole in the closed
48
+ * set.
49
+ */
50
+ export declare const ANNOTATION_KEYS: readonly string[];
51
+ export interface Diagnostic {
52
+ pointer: string;
53
+ message: string;
54
+ }
55
+ /**
56
+ * `INVALID <json-pointer> <message>`. The module's public contract.
57
+ *
58
+ * Pointers are JSON Pointers in RFC 6901 URI-fragment form, so the document
59
+ * root is `#` and a nested location is `#/gates/0/id`. The fragment form is
60
+ * chosen over the bare form for one mechanical reason: the bare pointer to
61
+ * the root is the EMPTY STRING, which would render this line with two
62
+ * consecutive spaces and make the three fields ambiguous to split. A
63
+ * diagnostic is reported at the pointer of the OFFENDING LOCATION, not of
64
+ * its parent, so a missing required property points at the property that
65
+ * should have been there.
66
+ */
67
+ export declare function formatDiagnostic(diagnostic: Diagnostic): string;
68
+ export declare function formatDiagnostics(diagnostics: Diagnostic[]): string[];
69
+ /**
70
+ * THE MESSAGE CONTRACT. One entry per way a value can fail. A replacement
71
+ * engine maps its own errors onto these; a test asserts on these and never
72
+ * on anything else.
73
+ */
74
+ export declare const DIAGNOSTIC_MESSAGES: {
75
+ type: (expected: string, observed: string) => string;
76
+ required: (name: string) => string;
77
+ additionalProperties: (name: string) => string;
78
+ enum: (value: string, permitted: string) => string;
79
+ const: (value: string, required: string) => string;
80
+ minimum: (value: string, minimum: string) => string;
81
+ minItems: (count: string, minimum: string) => string;
82
+ pattern: (value: string, pattern: string) => string;
83
+ /**
84
+ * Authored by src/gates/manifest.ts, which cannot express "keyed by name"
85
+ * in the closed keyword set. It lives in THIS table anyway (CR-811): the
86
+ * table's stated purpose is to give a future engine one place to map onto
87
+ * instead of a search through call sites, and a message that sits outside
88
+ * it defeats that purpose whatever module emits it.
89
+ */
90
+ duplicateId: (id: string) => string;
91
+ /** A $ref chain that returns to itself without consuming an instance. */
92
+ cyclicRef: (reference: string) => string;
93
+ unresolvedRef: (reference: string) => string;
94
+ };
95
+ export type SchemaDocument = Record<string, unknown>;
96
+ export type SchemaLoad = {
97
+ ok: true;
98
+ schema: SchemaDocument;
99
+ } | {
100
+ ok: false;
101
+ reason: string;
102
+ };
103
+ /** The document root, in URI-fragment form. */
104
+ export declare const ROOT_POINTER = "#";
105
+ /** The JSON type name of a value, as the `type` keyword uses it. */
106
+ export declare function jsonTypeOf(value: unknown): string;
107
+ /**
108
+ * Load a schema document, refusing loudly on any keyword outside the closed
109
+ * set. `name` appears in the reason so a caller with several documents knows
110
+ * which one failed.
111
+ */
112
+ export declare function loadSchema(document: unknown, name: string): SchemaLoad;
113
+ /**
114
+ * Validate an instance against a loaded schema. The returned list is sorted
115
+ * by (pointer, message), which is the deterministic order the contract
116
+ * promises and which is now produced by `sortDiagnostics` in the shared
117
+ * engine so the two modules cannot drift on it.
118
+ */
119
+ export declare function validate(schema: SchemaDocument, instance: unknown): Diagnostic[];
120
+ /** Validate and format in one step: the shape most callers want. */
121
+ export declare function validateToLines(schema: SchemaDocument, instance: unknown): string[];
@@ -0,0 +1,414 @@
1
+ /**
2
+ * THE MINIMAL IN-REPO SCHEMA VALIDATOR (kernel plan M2, M2-P1 step 4;
3
+ * M2-D-04; DR-0013 clause 6).
4
+ *
5
+ * M1 shipped zero runtime dependencies. DR-0013 decided the library
6
+ * question for M3 (Ajv 8.20.0 exact, Draft 2020-12) and made THIS module the
7
+ * seam: M3-P1 retires the engine below and keeps the boundary. That promise
8
+ * is only keepable if two things are true here, and both are enforced by
9
+ * this module's tests rather than asserted in prose:
10
+ *
11
+ * 1. THE DIAGNOSTIC CONTRACT IS `INVALID <json-pointer> <message>`, one
12
+ * line per failure, in a deterministic order. That string is this
13
+ * module's public output. Everything else about how a failure was
14
+ * found is an implementation detail and no test may assert on it.
15
+ * 2. THE MESSAGE TEXTS BELOW ARE THE CONTRACT, not this engine's wording.
16
+ * They are listed in `DIAGNOSTIC_MESSAGES` so a future engine has one
17
+ * table to map onto instead of a search through call sites.
18
+ *
19
+ * Without those two, DR-0013's "all existing M2 validation tests are re-run
20
+ * against it" would be a rewrite of the tests rather than an engine swap,
21
+ * and "the module boundary is preserved" would be false at the moment it was
22
+ * acted on.
23
+ *
24
+ * DETERMINISTIC ORDERING, stated so a replacement engine can reproduce it:
25
+ * every diagnostic is collected during a traversal that visits object
26
+ * properties in sorted key order and array items in index order, and the
27
+ * collected list is then sorted by (pointer, message) with ASCII
28
+ * lexicographic comparison. The final sort is what makes the order a
29
+ * property of the CONTRACT rather than of the traversal, so an engine that
30
+ * walks differently still emits the same lines in the same order.
31
+ *
32
+ * THE CLOSED KEYWORD SET, AND WHY IT IS LOUD (M2-D-04). Ten validation
33
+ * keywords plus local `$ref`. A schema document containing any other
34
+ * keyword is a LOAD ERROR naming the keyword and its location. It is never
35
+ * ignored, because a validator that silently skips `oneOf` reports a
36
+ * document valid while never having checked the constraint that mattered,
37
+ * which is the same shape as "green by omission" one layer down. The set is
38
+ * deliberately NOT grown to anticipate M3's five artifact schemas; DR-0013
39
+ * decided that question against extension.
40
+ */
41
+ import { sortDiagnostics, validateInstance, } from "../validate.js";
42
+ /** The ten validation keywords plus local `$ref`. Nothing else validates. */
43
+ export const VALIDATION_KEYWORDS = [
44
+ "type",
45
+ "required",
46
+ "properties",
47
+ "additionalProperties",
48
+ "enum",
49
+ "items",
50
+ "minimum",
51
+ "minItems",
52
+ "pattern",
53
+ "const",
54
+ "$ref",
55
+ ];
56
+ /**
57
+ * Keys that carry no constraint and are permitted for documentation and for
58
+ * holding subschemas. They are listed separately from the validation
59
+ * keywords on purpose: a reader must be able to see that permitting
60
+ * `description` is a decision about annotations and not a hole in the closed
61
+ * set.
62
+ */
63
+ export const ANNOTATION_KEYS = [
64
+ "$schema",
65
+ "$id",
66
+ "title",
67
+ "description",
68
+ "$defs",
69
+ ];
70
+ /**
71
+ * `INVALID <json-pointer> <message>`. The module's public contract.
72
+ *
73
+ * Pointers are JSON Pointers in RFC 6901 URI-fragment form, so the document
74
+ * root is `#` and a nested location is `#/gates/0/id`. The fragment form is
75
+ * chosen over the bare form for one mechanical reason: the bare pointer to
76
+ * the root is the EMPTY STRING, which would render this line with two
77
+ * consecutive spaces and make the three fields ambiguous to split. A
78
+ * diagnostic is reported at the pointer of the OFFENDING LOCATION, not of
79
+ * its parent, so a missing required property points at the property that
80
+ * should have been there.
81
+ */
82
+ export function formatDiagnostic(diagnostic) {
83
+ return `INVALID ${diagnostic.pointer} ${diagnostic.message}`;
84
+ }
85
+ export function formatDiagnostics(diagnostics) {
86
+ return diagnostics.map(formatDiagnostic);
87
+ }
88
+ /**
89
+ * THE MESSAGE CONTRACT. One entry per way a value can fail. A replacement
90
+ * engine maps its own errors onto these; a test asserts on these and never
91
+ * on anything else.
92
+ */
93
+ export const DIAGNOSTIC_MESSAGES = {
94
+ type: (expected, observed) => `expected type ${expected} but found ${observed}`,
95
+ required: (name) => `required property ${name} is missing`,
96
+ additionalProperties: (name) => `property ${name} is not permitted here`,
97
+ enum: (value, permitted) => `value ${value} is not one of the permitted values ${permitted}`,
98
+ const: (value, required) => `value ${value} does not equal the required constant ${required}`,
99
+ minimum: (value, minimum) => `value ${value} is below the minimum ${minimum}`,
100
+ minItems: (count, minimum) => `array has ${count} items, fewer than the required minimum ${minimum}`,
101
+ pattern: (value, pattern) => `value ${value} does not match the required pattern ${pattern}`,
102
+ /**
103
+ * Authored by src/gates/manifest.ts, which cannot express "keyed by name"
104
+ * in the closed keyword set. It lives in THIS table anyway (CR-811): the
105
+ * table's stated purpose is to give a future engine one place to map onto
106
+ * instead of a search through call sites, and a message that sits outside
107
+ * it defeats that purpose whatever module emits it.
108
+ */
109
+ duplicateId: (id) => `gate id ${id} is declared more than once`,
110
+ /** A $ref chain that returns to itself without consuming an instance. */
111
+ cyclicRef: (reference) => `schema reference ${reference} is cyclic`,
112
+ unresolvedRef: (reference) => `schema reference ${reference} does not resolve`,
113
+ };
114
+ /**
115
+ * Own-property test. `properties["__proto__"]` resolves through the prototype
116
+ * chain to `Object.prototype`, which is an object, so a naive lookup treats
117
+ * `__proto__` as a DECLARED property and lets it through
118
+ * `additionalProperties: false` (CR-808). Ajv rejects it, so this was also a
119
+ * seam divergence.
120
+ */
121
+ function ownProperty(container, name) {
122
+ return Object.prototype.hasOwnProperty.call(container, name)
123
+ ? container[name]
124
+ : undefined;
125
+ }
126
+ function pointerSegment(segment) {
127
+ return segment.replace(/~/g, "~0").replace(/\//g, "~1");
128
+ }
129
+ function childPointer(pointer, segment) {
130
+ return `${pointer}/${pointerSegment(segment)}`;
131
+ }
132
+ /** The document root, in URI-fragment form. */
133
+ export const ROOT_POINTER = "#";
134
+ function isPlainObject(value) {
135
+ return typeof value === "object" && value !== null && !Array.isArray(value);
136
+ }
137
+ /** The JSON type name of a value, as the `type` keyword uses it. */
138
+ export function jsonTypeOf(value) {
139
+ if (value === null) {
140
+ return "null";
141
+ }
142
+ if (Array.isArray(value)) {
143
+ return "array";
144
+ }
145
+ const primitive = typeof value;
146
+ if (primitive === "number") {
147
+ return Number.isInteger(value) ? "integer" : "number";
148
+ }
149
+ if (primitive === "boolean") {
150
+ return "boolean";
151
+ }
152
+ if (primitive === "string") {
153
+ return "string";
154
+ }
155
+ return "object";
156
+ }
157
+ const PERMITTED_TYPES = [
158
+ "object",
159
+ "array",
160
+ "string",
161
+ "number",
162
+ "integer",
163
+ "boolean",
164
+ "null",
165
+ ];
166
+ /**
167
+ * Walk a schema document and refuse anything outside the closed set. The
168
+ * walk knows WHERE subschemas live (`properties/*`, `items`, `$defs/*` and
169
+ * the root), so a property literally named "oneOf" inside `properties` is a
170
+ * property name and not a keyword, and is not confused for one.
171
+ */
172
+ function checkSchemaNode(node, pointer, problems) {
173
+ if (!isPlainObject(node)) {
174
+ problems.push(`${pointer} is not a schema object`);
175
+ return;
176
+ }
177
+ for (const key of Object.keys(node).sort()) {
178
+ if (VALIDATION_KEYWORDS.includes(key) || ANNOTATION_KEYS.includes(key)) {
179
+ continue;
180
+ }
181
+ problems.push(`unsupported schema keyword ${key} at ${pointer}`);
182
+ }
183
+ const type = node["type"];
184
+ if (type !== undefined) {
185
+ if (typeof type !== "string" || !PERMITTED_TYPES.includes(type)) {
186
+ problems.push(`unsupported type value at ${pointer}: this validator accepts a single type name from ${PERMITTED_TYPES.join(", ")}`);
187
+ }
188
+ }
189
+ const additional = node["additionalProperties"];
190
+ if (additional !== undefined && typeof additional !== "boolean") {
191
+ problems.push(`unsupported additionalProperties at ${pointer}: this validator accepts a boolean only`);
192
+ }
193
+ const required = node["required"];
194
+ if (required !== undefined && !Array.isArray(required)) {
195
+ problems.push(`required at ${pointer} is not an array`);
196
+ }
197
+ const enumeration = node["enum"];
198
+ if (enumeration !== undefined && !Array.isArray(enumeration)) {
199
+ problems.push(`enum at ${pointer} is not an array`);
200
+ }
201
+ const reference = node["$ref"];
202
+ if (reference !== undefined && typeof reference !== "string") {
203
+ problems.push(`$ref at ${pointer} is not a string`);
204
+ }
205
+ if (reference !== undefined) {
206
+ // CR-802. JSON Schema 2020-12 APPLIES keywords sitting beside `$ref`,
207
+ // and Ajv does too. This engine followed the reference and returned,
208
+ // dropping every sibling without a word: a KNOWN keyword in a position
209
+ // the validator silently ignores, which is the harder half of the
210
+ // attack the closed keyword set exists to stop, and a verdict change at
211
+ // the exact seam DR-0013 clause 6 promises M3-P1 can swap across.
212
+ //
213
+ // Refusing at load rather than implementing sibling application is the
214
+ // choice this module's philosophy already made everywhere else: a
215
+ // schema this engine cannot evaluate the way the specification says is
216
+ // rejected, never partially honoured. A schema that never uses the
217
+ // construct behaves identically under both engines.
218
+ const siblings = Object.keys(node)
219
+ .filter((key) => key !== "$ref" && VALIDATION_KEYWORDS.includes(key))
220
+ .sort();
221
+ if (siblings.length > 0) {
222
+ problems.push(`$ref at ${pointer} has sibling keyword(s) ${siblings.join(", ")}: ` +
223
+ "this validator does not apply keywords beside a $ref, and " +
224
+ "silently ignoring them would validate less than the schema says");
225
+ }
226
+ }
227
+ const properties = node["properties"];
228
+ if (properties !== undefined) {
229
+ if (!isPlainObject(properties)) {
230
+ problems.push(`properties at ${pointer} is not an object`);
231
+ }
232
+ else {
233
+ for (const name of Object.keys(properties).sort()) {
234
+ checkSchemaNode(ownProperty(properties, name), `${childPointer(pointer, "properties")}/${pointerSegment(name)}`, problems);
235
+ }
236
+ }
237
+ }
238
+ const defs = node["$defs"];
239
+ if (defs !== undefined) {
240
+ if (!isPlainObject(defs)) {
241
+ problems.push(`$defs at ${pointer} is not an object`);
242
+ }
243
+ else {
244
+ for (const name of Object.keys(defs).sort()) {
245
+ checkSchemaNode(ownProperty(defs, name), `${childPointer(pointer, "$defs")}/${pointerSegment(name)}`, problems);
246
+ }
247
+ }
248
+ }
249
+ const patternSource = node["pattern"];
250
+ if (patternSource !== undefined) {
251
+ if (typeof patternSource !== "string") {
252
+ problems.push(`pattern at ${pointer} is not a string`);
253
+ }
254
+ else {
255
+ // Compiled HERE so an unusable pattern is a load failure with a
256
+ // reason, not a throw escaping mid-validation. CR-801's derivation
257
+ // named this exact asymmetry: `new RegExp` on a CALLER-supplied
258
+ // pattern was guarded and `new RegExp` on a SCHEMA-supplied one was
259
+ // not, same call, same failure, one guarded.
260
+ try {
261
+ new RegExp(patternSource);
262
+ }
263
+ catch (error) {
264
+ problems.push(`pattern at ${pointer} is not a valid expression: ${error.message}`);
265
+ }
266
+ }
267
+ }
268
+ const items = node["items"];
269
+ if (items !== undefined) {
270
+ checkSchemaNode(items, childPointer(pointer, "items"), problems);
271
+ }
272
+ }
273
+ /**
274
+ * Load a schema document, refusing loudly on any keyword outside the closed
275
+ * set. `name` appears in the reason so a caller with several documents knows
276
+ * which one failed.
277
+ */
278
+ export function loadSchema(document, name) {
279
+ const problems = [];
280
+ checkSchemaNode(document, ROOT_POINTER, problems);
281
+ if (problems.length > 0) {
282
+ return {
283
+ ok: false,
284
+ reason: `${name}: ${problems.sort().join("; ")}`,
285
+ };
286
+ }
287
+ return { ok: true, schema: document };
288
+ }
289
+ function resolveRef(root, reference) {
290
+ // `#` is the whole document, which is how a recursive schema is normally
291
+ // written. It was unhandled until a control in test/gates.test.ts exercised
292
+ // a legitimate recursive schema and got "does not resolve"; the cycle fix
293
+ // that motivated the control would otherwise have shipped beside a gap
294
+ // that made every recursive schema unusable.
295
+ if (reference === "#") {
296
+ return root;
297
+ }
298
+ if (!reference.startsWith("#/")) {
299
+ return undefined;
300
+ }
301
+ let node = root;
302
+ for (const raw of reference.slice(2).split("/")) {
303
+ const segment = raw.replace(/~1/g, "/").replace(/~0/g, "~");
304
+ if (!isPlainObject(node)) {
305
+ return undefined;
306
+ }
307
+ node = ownProperty(node, segment);
308
+ }
309
+ return isPlainObject(node) ? node : undefined;
310
+ }
311
+ /* ------------------------------------------------------------------ */
312
+ /* THE ENGINE, RETIRED (DR-0013 clause 6, M3-P1 step 8). */
313
+ /* ------------------------------------------------------------------ */
314
+ /**
315
+ * WHAT WAS RETIRED AND WHAT WAS KEPT, stated because the distinction is the
316
+ * whole of DR-0013 clause 6.
317
+ *
318
+ * RETIRED: keyword SEMANTICS. `type`, `required`, `properties`,
319
+ * `additionalProperties`, `enum`, `items`, `minimum`, `minItems`, `pattern`
320
+ * and `const` are no longer evaluated here. `validate` hands the schema and
321
+ * the instance to `src/validate.ts`, which is Ajv 8.20.0 under the policies
322
+ * DR-0013 clause 4 fixed. Two engines with potentially different semantics
323
+ * are not maintained.
324
+ *
325
+ * KEPT, and each for a reason DR-0013 names:
326
+ *
327
+ * 1. THE MODULE BOUNDARY. `loadSchema`, `validate`, `validateToLines`,
328
+ * `formatDiagnostic`, `formatDiagnostics` and `DIAGNOSTIC_MESSAGES` are
329
+ * unchanged in name, signature and meaning, so every M2 caller and every
330
+ * M2 test is untouched.
331
+ * 2. THE CLOSED KEYWORD SET, as a POLICY LINTER. `loadSchema` still refuses
332
+ * a gate schema containing a keyword outside M2-D-04's set. DR-0013
333
+ * clause 7 says this explicitly: prohibiting otherwise-valid but
334
+ * unapproved keywords "is a small schema-aware POLICY LINTER, never a
335
+ * reimplementation of keyword semantics". Under Ajv `oneOf` compiles
336
+ * fine, so without this linter M2's gate schemas would silently acquire
337
+ * a vocabulary nobody approved.
338
+ * 3. THE REFERENCE POLICY. Measured 2026-08-08 with the pinned Ajv:
339
+ * compiling `{type:"object",properties:{x:{$ref:"#/$defs/a"}},
340
+ * $defs:{a:{$ref:"#/$defs/a"}}}` raises
341
+ * `RangeError: Maximum call stack size exceeded`. That is CR-807's
342
+ * failure wearing a different engine's clothes, so the cycle and
343
+ * unresolved-reference analysis stays HERE, in front of the engine,
344
+ * where it produces a diagnostic instead of a stack overflow. It
345
+ * evaluates no other keyword.
346
+ */
347
+ /**
348
+ * Reference-only joint walk. Follows `$ref` and descends `properties` and
349
+ * `items`, and does nothing else. `refChain` holds the references followed
350
+ * since the last INSTANCE node was consumed, so a legitimate recursive schema
351
+ * (which always consumes an instance node between two follows) is not
352
+ * mistaken for a cycle. This is the same rule the retired engine used and the
353
+ * same diagnostics, because both are contract.
354
+ */
355
+ function collectReferenceDiagnostics(root, schema, instance, pointer, into, refChain, seenNodes) {
356
+ const reference = schema["$ref"];
357
+ if (typeof reference === "string") {
358
+ if (refChain.includes(reference)) {
359
+ into.push({ pointer, message: DIAGNOSTIC_MESSAGES.cyclicRef(reference) });
360
+ return;
361
+ }
362
+ const target = resolveRef(root, reference);
363
+ if (target === undefined) {
364
+ into.push({
365
+ pointer,
366
+ message: DIAGNOSTIC_MESSAGES.unresolvedRef(reference),
367
+ });
368
+ return;
369
+ }
370
+ collectReferenceDiagnostics(root, target, instance, pointer, into, [...refChain, reference], seenNodes);
371
+ return;
372
+ }
373
+ if (Array.isArray(instance)) {
374
+ const items = schema["items"];
375
+ if (isPlainObject(items)) {
376
+ for (let index = 0; index < instance.length; index += 1) {
377
+ collectReferenceDiagnostics(root, items, instance[index], `${pointer}/${index}`, into, [], seenNodes);
378
+ }
379
+ }
380
+ return;
381
+ }
382
+ if (isPlainObject(instance)) {
383
+ const properties = isPlainObject(schema["properties"])
384
+ ? schema["properties"]
385
+ : {};
386
+ for (const name of Object.keys(instance).sort()) {
387
+ const subschema = ownProperty(properties, name);
388
+ if (isPlainObject(subschema)) {
389
+ collectReferenceDiagnostics(root, subschema, ownProperty(instance, name), childPointer(pointer, name), into, [], seenNodes);
390
+ }
391
+ }
392
+ }
393
+ }
394
+ /**
395
+ * Validate an instance against a loaded schema. The returned list is sorted
396
+ * by (pointer, message), which is the deterministic order the contract
397
+ * promises and which is now produced by `sortDiagnostics` in the shared
398
+ * engine so the two modules cannot drift on it.
399
+ */
400
+ export function validate(schema, instance) {
401
+ const referenceProblems = [];
402
+ collectReferenceDiagnostics(schema, schema, instance, ROOT_POINTER, referenceProblems, [], new Set());
403
+ if (referenceProblems.length > 0) {
404
+ // A schema whose references do not resolve, or loop, cannot be compiled
405
+ // at all: the engine would throw. Report and stop, exactly as the retired
406
+ // engine returned at the offending reference node.
407
+ return sortDiagnostics(referenceProblems);
408
+ }
409
+ return validateInstance(schema, instance);
410
+ }
411
+ /** Validate and format in one step: the shape most callers want. */
412
+ export function validateToLines(schema, instance) {
413
+ return formatDiagnostics(validate(schema, instance));
414
+ }
@@ -0,0 +1,84 @@
1
+ import type { Fleet } from "./fleet.ts";
2
+ /**
3
+ * Turn-end hook (kernel plan v1, M1-P4 step 3; R-082b, PR-209).
4
+ *
5
+ * The hook is a generated script in the TASK directory (never inside the
6
+ * worktree, FM-059) that the executor invokes when the payload command
7
+ * exits. It writes tasks/<id>/turn-end as JSON:
8
+ *
9
+ * {"endedAt": "<ISO-8601>", "exitCode": <number>}
10
+ *
11
+ * That file is the M1-P5 watcher's wake signal. It is a notification,
12
+ * not the task's state: meta.json status plus this exit code together
13
+ * are the single current-state authority (plan constraint C-1), and a
14
+ * MISSING turn-end file never means success (tuition T-002: an agent
15
+ * that dies quietly leaves no turn-end and a dirty worktree).
16
+ *
17
+ * The hook takes the payload's exit code as its single argument, so any
18
+ * adapter (the M1 subprocess adapter, and the window or cloud-session
19
+ * adapters of the M4 era) can invoke it without knowing anything else
20
+ * about the task. It is written with the .mjs extension so it parses as
21
+ * ESM regardless of the nearest package.json, since it runs inside the
22
+ * fleet home rather than inside this package.
23
+ */
24
+ /** The generated hook script for a task. */
25
+ export declare function turnEndHookPath(fleet: Fleet, taskId: string): string;
26
+ /**
27
+ * Generate the hook script. The turn-end path is baked in as a literal,
28
+ * so the hook needs no fleet resolution.
29
+ *
30
+ * THE CHILD-OBSERVED POINTER RECORD (CR-B-001, the half that closes the hole
31
+ * rather than the half that stops mis-asserting it).
32
+ *
33
+ * `observeNames`, when given, is baked in as a literal array and the hook
34
+ * writes an `env` object holding what each of those names ACTUALLY IS in the
35
+ * environment the hook was launched with, or `null` where the name is unset.
36
+ * The kernel compares those against the harness-owned paths it handed over.
37
+ *
38
+ * WHY THIS IS STRONGER THAN AN ADAPTER'S REPORT AND WHY IT IS NOT PROOF.
39
+ * This script is written BY THE KERNEL and, for every adapter that honours
40
+ * the documented contract, runs in the SAME environment as the payload (the
41
+ * built-in adapter spreads the same `request.env` into both spawnSync calls,
42
+ * and M2R-004 edit 4 is the record of why a second unscrubbed launch is
43
+ * itself the leak). Against an adapter that does not invoke this script at
44
+ * all it proves nothing, and the record no longer says otherwise.
45
+ *
46
+ * THE COST SENTENCE THAT STOOD HERE IS WITHDRAWN, BECAUSE IT WAS REFUTED BY
47
+ * MEASUREMENT (CR-F-CRED-001, MEDIUM).
48
+ *
49
+ * It read: an adapter that quietly reverted `HOME` for the payload "has to
50
+ * revert it for the payload and NOT for the hook, which means launching two
51
+ * children with two different environments and is a substantially different
52
+ * act from passing a mutated copy once". Two children is one way to do it and
53
+ * it is not the cheap way. The cheap way is ONE child with the mutated
54
+ * environment plus a single `writeFileSync` of the turn-end path, which is
55
+ * STRICTLY LESS work than the honest path, because the honest path also
56
+ * spawns the hook. The turn-end path is handed to the adapter beside
57
+ * `hookPath`, and this generated script names it as a literal, so an adapter
58
+ * that never runs it can still produce a byte-identical record.
59
+ *
60
+ * WHY A NONCE DOES NOT CLOSE THIS, stated because it is the obvious repair and
61
+ * it was considered and refused rather than overlooked. Baking a per-task
62
+ * secret into this script and requiring it in the record moves the forgery
63
+ * from "know the path" to "read the file", and the adapter is HANDED the path
64
+ * of this file: it runs at the same uid, on the same filesystem, in a
65
+ * directory it must be able to read to invoke the hook at all. A guard whose
66
+ * condition the adversary can satisfy by reading one file is green and
67
+ * worthless, which is this repository's own recorded shape (T-008's
68
+ * postscript, the red-witness rule one level up). No artifact this script can
69
+ * write is unforgeable by a party that can read this script.
70
+ *
71
+ * So the repair is on the RECORD rather than on the check: the value is
72
+ * `turn-end-record`, it names the artifact the values were read from, and
73
+ * `CredentialHandoverRecord` (src/task.ts) says the artifact is
74
+ * adapter-reachable. A dishonest adapter is still not caught here, and that
75
+ * residue is declared at delivery/work-history/m4-p8.md item 3; what changed
76
+ * is that `meta.json` no longer positively asserts a child-side observation
77
+ * that no child made.
78
+ *
79
+ * The hook still reads no environment it was not told to read, and a hook
80
+ * generated with no `observeNames` behaves exactly as it did before.
81
+ */
82
+ export declare function renderTurnEndHook(turnEndFile: string, observeNames?: readonly string[]): string;
83
+ /** Write the hook for a task and return its path. */
84
+ export declare function writeTurnEndHook(fleet: Fleet, taskId: string, observeNames?: readonly string[]): string;