@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,430 @@
1
+ /**
2
+ * `tiphys validate --type <t> [--context <dir>] <file>` (kernel plan M3,
3
+ * M3-P1 step 8).
4
+ *
5
+ * Exit codes:
6
+ * 0 the instance passed schema validation and every derived check
7
+ * 1 at least one violation, or a derived check that could not run
8
+ * 64 usage error (BSD sysexits EX_USAGE), including `--type auto` on an
9
+ * instance with no `kind` field
10
+ *
11
+ * TWO STAGES, NEVER CONFLATED (DR-0013 YAML clause 3). The file is DECODED
12
+ * first and the decoded value is VALIDATED second. A decode failure names the
13
+ * decode; a validation failure names the pointer. Neither produces a stack
14
+ * trace on any stream, which is step 8b's policy applied at the site that
15
+ * needs it most: this command's ordinary input is a hand-authored file that
16
+ * will routinely be malformed, and a validator that answers malformed YAML
17
+ * with a stack trace is a validator nobody trusts.
18
+ *
19
+ * THE PATH IS OPERATOR-SUPPLIED (D-M3-27). It is classified before it is
20
+ * opened, so a named pipe is refused with the observed entry type instead of
21
+ * blocking this command forever. Same for the `--context` directory. This is
22
+ * the M1-P5 class (CR-520, twelve paths, four fix rounds) applied to the
23
+ * first kernel command whose ordinary input is a path an operator wrote.
24
+ */
25
+ import { readdirSync } from "node:fs";
26
+ import { fileURLToPath } from "node:url";
27
+ import { dirname, join } from "node:path";
28
+ import { runChecks } from "../checks.js";
29
+ import { outputContractDiagnostics, splitFrontmatter } from "../roles.js";
30
+ import { roleBriefBodyDiagnostics } from "./brief.js";
31
+ import { classifyContextDirectory, decodeDocument, formatDiagnostics, readOperatorPath, validateInstance, } from "../validate.js";
32
+ /** Exit code for usage errors, per BSD sysexits EX_USAGE. */
33
+ export const EX_USAGE = 64;
34
+ /**
35
+ * The artifact types this milestone's validator knows, and the schema
36
+ * document each resolves to.
37
+ *
38
+ * REGISTERED HERE, EXTENDED BY EVERY LATER PHASE (M3R-001). A phase that
39
+ * introduces an artifact type adds its row here and to the `auto` resolver in
40
+ * the same step, so a type whose schema ships but which `--type` cannot name
41
+ * is not a state this command can be in.
42
+ */
43
+ export const TYPE_TABLE = new Map([
44
+ ["plan", "plan.schema.json"],
45
+ ["charter", "charter.schema.json"],
46
+ ["decision-record", "decision-record.schema.json"],
47
+ ["status-line", "status-line.schema.json"],
48
+ /* M3-P2 step 6. The registry's own `kind` field is `gate-registry`, so
49
+ adding this row extends `--type` and `resolveAutoType` in one act, which
50
+ is what M3R-001 means by registering the type in the same step that ships
51
+ the schema. */
52
+ ["gate-registry", "gate-registry.schema.json"],
53
+ /* M3-P3 step 6. Both documents carry their own `kind`, so each row extends
54
+ `--type` and `resolveAutoType` in one act (M3R-001). The assurance-modes
55
+ row is also what makes this phase's four derived checks reachable: they are
56
+ registered for type `assurance-modes` and nothing runs them until the type
57
+ resolves. */
58
+ ["assurance-modes", "assurance-modes.schema.json"],
59
+ ["role-model-config", "role-model-config.schema.json"],
60
+ /* M3-P4 step 5. Each document carries its own `kind`, so each row extends
61
+ `--type` and `resolveAutoType` in one act (M3R-001). `work-history` also
62
+ has a COMPANION row below, because its shared honesty definitions live in
63
+ report.schema.json rather than being restated. */
64
+ ["report", "report.schema.json"],
65
+ ["final-report", "final-report.schema.json"],
66
+ ["work-history", "work-history.schema.json"],
67
+ /* M3-P5 steps 1 and 5. `finding` carries its own `kind`, so its row extends
68
+ `--type` and `resolveAutoType` in one act (M3R-001). `role-brief` DOES
69
+ NOT and cannot: `--type auto` reads `kind` off the DECODED instance, and
70
+ decoding a role brief means knowing first that it is markdown with a
71
+ frontmatter fence rather than a YAML document, so the type has to be
72
+ named before the document can be decoded at all. The half of M3R-001
73
+ that the finding was actually about, a schema that ships without its
74
+ `--type` row and forces an implementer to edit an undeclared file, is
75
+ satisfied by this row. */
76
+ ["role-brief", "role-brief.schema.json"],
77
+ ["finding", "finding.schema.json"],
78
+ /* M3-P6 criterion 8. The seed index carries its own `kind`, so this row
79
+ extends `--type` and `resolveAutoType` in one act (M3R-001). The SCHEMA
80
+ ships here and the GENERATOR ships at M3-P8, which is D-M3-23's forward
81
+ reference: M3-P8 edits the schema to add `machine-readable-form` and
82
+ replaces the seed's contents with the generated projection, and neither of
83
+ those acts touches this row. */
84
+ ["mechanism-index", "mechanism-index.schema.json"],
85
+ /* M3-P7 step 6b. Both documents carry their own `kind`, so each row extends
86
+ `--type` and `resolveAutoType` in one act (M3R-001). The `checklist` row
87
+ is also what makes this phase's derived checks reachable: three of the
88
+ five are registered for type `checklist` or `verdict` and nothing runs
89
+ them until the type resolves, which is the same coupling the
90
+ assurance-modes row records above. */
91
+ ["checklist", "checklist.schema.json"],
92
+ ["verdict", "verdict.schema.json"],
93
+ /* M3-P8 step 8. A tuition entry carries its own `kind`, so this row extends
94
+ `--type` and `resolveAutoType` in one act (M3R-001). It is also what makes
95
+ this phase's two derived checks reachable: `tuition-target-exists` and
96
+ `mechanism-rule-evidence-resolves` are registered for type `tuition` and
97
+ nothing runs them until the type resolves. */
98
+ ["tuition", "tuition.schema.json"],
99
+ /* M4-P25 criterion 4. The fleet's `cutover.json`. It carries NO `kind`
100
+ field, so this row extends `--type` and NOT `resolveAutoType`; `role-brief`
101
+ above is the standing precedent for that half of M3R-001, and the half the
102
+ rule is actually about, a schema that ships without a `--type` row and
103
+ forces a later implementer to edit an undeclared file, is satisfied here.
104
+ The document is validated programmatically on every write as well
105
+ (src/cutover.ts:253), which is what criterion 4 means by validated by the
106
+ schema rather than by the command. */
107
+ ["cutover-state", "cutover-state.schema.json"],
108
+ /* M4-P3 criterion 4. tasks/<id>/executor.json, the launch record an
109
+ ExecutorAdapter writes.
110
+
111
+ THIS ROW IS THE WHOLE REGISTRATION, AND `resolveAutoType` IS NOT A SECOND
112
+ PLACE TO EDIT: that function answers `--type auto` by reading the
113
+ instance's `kind` and looking it up in THIS MAP, so a row added here is
114
+ the auto resolver's source of truth as well. M3R-001's requirement that a
115
+ shipped schema not arrive without its `--type` row is satisfied by the
116
+ row; nothing else in this module holds a type list.
117
+
118
+ WHAT THE ROW CANNOT DO, stated here rather than left to be discovered.
119
+ An executor record carries no `kind` field, because M4-P3's criterion 6
120
+ fixes the record's fields and `kind` is not among them, so `--type auto`
121
+ on a real executor.json is a USAGE error naming the missing kind. That is
122
+ the `role-brief` situation above reached by a different route: there the
123
+ document cannot be decoded before the type is known, here the decoded
124
+ document does not say what it is. Both are types a caller must NAME. The
125
+ property is asserted by a registered test rather than trusted, so a later
126
+ phase that gives the record a `kind` reddens this comment instead of
127
+ leaving it stale. */
128
+ ["executor-record", "executor-record.schema.json"],
129
+ /* M4-P7 criterion 1. tasks/<id>/model-resolution.json, the record a harness
130
+ adapter writes at TURN END saying which tier was requested, which the
131
+ charter resolved, and which model family served the turn.
132
+
133
+ ONE ROW SERVES BOTH REGISTRATIONS, which is the property the brief for
134
+ this phase asks to be got right and the row above states the mechanism
135
+ for: `resolveAutoType` answers `--type auto` by reading the instance's
136
+ `kind` and looking it up in THIS MAP. The difference from the executor
137
+ record is that a model-resolution record DOES carry a `kind`, required and
138
+ `const`, so `--type auto` resolves a real record rather than raising the
139
+ usage error the row above documents. `test/model-resolution.test.ts`
140
+ asserts both halves against the shipped CLI rather than against this
141
+ comment.
142
+
143
+ THE DERIVED CHECKS COME WITH THE ROW AND ARE THE REASON IT MATTERS. The
144
+ schema is Kind A and reaches presence; `model-resolution-subject-echo` in
145
+ src/checks.ts is Kind B and reaches agreement between sibling fields, and
146
+ nothing runs it until the type resolves here. */
147
+ ["model-resolution", "model-resolution.schema.json"],
148
+ /* M4-P9 step 4. <fleet>/write-bypass.json, the infrastructure-hotfix bypass
149
+ the project-write block reads.
150
+
151
+ THE ROW IS WHAT MAKES THE DECLARATION CHECKABLE BEFORE IT IS TRUSTED. The
152
+ hook reads the document defensively and refuses on anything it cannot use,
153
+ which is the right behaviour at decision time and a terrible way to find
154
+ out you mistyped an expiry: the orchestrator learns at the moment its own
155
+ hotfix is refused. With this row an author runs `tiphys validate --type
156
+ write-bypass <file>` and is told which pointer is wrong, before the
157
+ document is in front of a hook.
158
+
159
+ A bypass declaration DOES carry a `kind`, required and `const`, so one row
160
+ serves `--type` and `resolveAutoType` together (M3R-001), unlike the
161
+ executor record above.
162
+
163
+ WHAT THE ROW CANNOT DO, stated rather than left to be discovered: the
164
+ schema reaches the SHAPE and never the WINDOW. Whether `expiresAt` is in
165
+ the future is a comparison against a clock, and `decideWrite` takes `now`
166
+ as a parameter precisely so that comparison is testable at the boundary
167
+ rather than hidden inside a validator. */
168
+ ["write-bypass", "write-bypass.schema.json"],
169
+ ]);
170
+ /**
171
+ * COMPANION SCHEMAS, declared beside the type table (M3-P4 step 3).
172
+ *
173
+ * A type listed here is compiled with the named sibling documents registered
174
+ * alongside it, so a `$ref` that leaves the document resolves. THE SET IS
175
+ * DECLARED, NOT DISCOVERED: `src/validate.ts` never reads a reference and
176
+ * fetches what it names, so a `$ref` to a document absent from this table
177
+ * still fails compilation with `unresolvedRef`. That is the property DR-0013
178
+ * clause 4 protects, kept while letting two artifact types share ONE
179
+ * definition of the honesty contract instead of two that can drift.
180
+ *
181
+ * The reference in `work-history.schema.json` is RELATIVE
182
+ * (`report.schema.json#/$defs/claim`), so it resolves against that document's
183
+ * `$id` to `https://tiphys.dev/schemas/report.schema.json`, which is exactly
184
+ * the companion's own `$id`.
185
+ */
186
+ export const COMPANION_TABLE = new Map([
187
+ ["work-history", ["report"]],
188
+ ]);
189
+ /** The companion schema documents a type is compiled with, in declared order. */
190
+ export function companionsFor(type) {
191
+ return (COMPANION_TABLE.get(type) ?? []).map((companion) => loadTypeSchema(companion));
192
+ }
193
+ /**
194
+ * Locate the shipped `schemas/` directory by walking UP from this module.
195
+ *
196
+ * The depth differs between the two layouts this code runs in: from source it
197
+ * is `src/commands/` and the directory is two levels up, and from the built
198
+ * entry it is `dist/src/commands/` and the directory is three levels up
199
+ * beside the package root. Counting `..` would therefore be right in exactly
200
+ * one of them, which is the kind of silent, layout-dependent break M2's own
201
+ * schema resolution comment warns about. Walking up and TESTING for the
202
+ * directory is right in both, and in a relocated copy as well.
203
+ */
204
+ export function schemasDirectory() {
205
+ let directory = dirname(fileURLToPath(import.meta.url));
206
+ for (let depth = 0; depth < 8; depth += 1) {
207
+ const candidate = join(directory, "schemas");
208
+ try {
209
+ const entries = readdirSync(candidate);
210
+ if (entries.some((name) => name.endsWith(".schema.json"))) {
211
+ return candidate;
212
+ }
213
+ }
214
+ catch {
215
+ /* not here; keep walking */
216
+ }
217
+ const parent = dirname(directory);
218
+ if (parent === directory) {
219
+ break;
220
+ }
221
+ directory = parent;
222
+ }
223
+ throw new Error("the shipped schemas/ directory was not found above this module; the installation is incomplete");
224
+ }
225
+ const schemaCache = new Map();
226
+ /** Read and decode a shipped schema document. */
227
+ export function loadTypeSchema(type) {
228
+ const cached = schemaCache.get(type);
229
+ if (cached !== undefined) {
230
+ return cached;
231
+ }
232
+ const filename = TYPE_TABLE.get(type);
233
+ if (filename === undefined) {
234
+ throw new Error(`no schema is registered for type ${type}`);
235
+ }
236
+ const path = join(schemasDirectory(), filename);
237
+ const read = readOperatorPath(path);
238
+ if (!read.ok) {
239
+ throw new Error(read.reason);
240
+ }
241
+ const decoded = decodeDocument(read.body, path);
242
+ if (!decoded.ok) {
243
+ throw new Error(decoded.reason);
244
+ }
245
+ const document = decoded.value;
246
+ schemaCache.set(type, document);
247
+ return document;
248
+ }
249
+ /**
250
+ * Resolve `--type auto` from the instance's `kind` field. An instance with no
251
+ * `kind`, or with a `kind` no schema is registered for, is a USAGE error: the
252
+ * caller asked the command to work out which contract applies and it cannot,
253
+ * which is not the same as the document failing that contract.
254
+ */
255
+ export function resolveAutoType(instance) {
256
+ if (typeof instance !== "object" || instance === null || Array.isArray(instance)) {
257
+ return undefined;
258
+ }
259
+ const kind = instance["kind"];
260
+ if (typeof kind !== "string" || !TYPE_TABLE.has(kind)) {
261
+ return undefined;
262
+ }
263
+ return kind;
264
+ }
265
+ function usage() {
266
+ const types = [...TYPE_TABLE.keys()].sort().join(" | ");
267
+ return `usage: tiphys validate --type <${types} | auto> [--context <dir>] <file>`;
268
+ }
269
+ function parseArgs(argv) {
270
+ const options = {};
271
+ for (let index = 0; index < argv.length; index += 1) {
272
+ const argument = argv[index];
273
+ if (argument === "--type" || argument === "--context") {
274
+ const value = argv[index + 1];
275
+ if (value === undefined || value.startsWith("--")) {
276
+ return { usageError: `${argument} requires a value` };
277
+ }
278
+ if (argument === "--type") {
279
+ options.type = value;
280
+ }
281
+ else {
282
+ options.context = value;
283
+ }
284
+ index += 1;
285
+ continue;
286
+ }
287
+ if (argument.startsWith("--")) {
288
+ return { usageError: `unknown option ${argument}` };
289
+ }
290
+ if (options.file !== undefined) {
291
+ return { usageError: "exactly one file argument is accepted" };
292
+ }
293
+ options.file = argument;
294
+ }
295
+ if (options.type === undefined) {
296
+ return { usageError: "--type is required" };
297
+ }
298
+ if (options.file === undefined) {
299
+ return { usageError: "a file argument is required" };
300
+ }
301
+ return { options };
302
+ }
303
+ /**
304
+ * Validate one role brief: frontmatter against `role-brief.schema.json`, then
305
+ * the clause round trip over the include-expanded body (M3-P5 criteria 1 and
306
+ * 6b). Kept as its own function rather than folded into `cmdValidate`'s main
307
+ * path, because a role brief is the one artifact type whose file is not a
308
+ * YAML document and conflating the two decode paths is how a horizontal rule
309
+ * becomes a contract.
310
+ */
311
+ function validateRoleBrief(file, body, context) {
312
+ const split = splitFrontmatter(body, file);
313
+ if (!split.ok) {
314
+ process.stderr.write(`tiphys validate: ${split.reason}\n`);
315
+ return 1;
316
+ }
317
+ const decoded = decodeDocument(split.frontmatter, `${file} frontmatter`);
318
+ if (!decoded.ok) {
319
+ process.stderr.write(`tiphys validate: ${decoded.reason}\n`);
320
+ return 1;
321
+ }
322
+ const schema = loadTypeSchema("role-brief");
323
+ const diagnostics = validateInstance(schema, decoded.value);
324
+ if (diagnostics.length > 0) {
325
+ for (const line of formatDiagnostics(diagnostics)) {
326
+ process.stdout.write(`${line}\n`);
327
+ }
328
+ return 1;
329
+ }
330
+ const frontmatter = decoded.value;
331
+ const clauses = Array.isArray(frontmatter["clauses"])
332
+ ? frontmatter["clauses"].map((entry) => String(entry))
333
+ : [];
334
+ const roundTrip = roleBriefBodyDiagnostics(file, split.body, clauses);
335
+ if (!roundTrip.ok) {
336
+ process.stderr.write(`tiphys validate: ${roundTrip.reason}\n`);
337
+ return 1;
338
+ }
339
+ for (const line of roundTrip.lines) {
340
+ process.stdout.write(`${line}\n`);
341
+ }
342
+ /* THE OUTPUT CONTRACT (M3-P5 fix round 1). Every declared output type whose
343
+ schema is registered must have that schema on mandated-reading. Wired
344
+ HERE, and the reason is that TYPE_TABLE is the map and it lives in this
345
+ module: passing the lookup in as a function keeps src/roles.ts free of an
346
+ import back into this command, so the two do not become a cycle. The
347
+ lookup is the SAME table `--type` resolves against, so the check cannot
348
+ drift from what the validator would actually compile. */
349
+ const outputContract = formatDiagnostics(outputContractDiagnostics(Array.isArray(frontmatter["outputs"])
350
+ ? frontmatter["outputs"].map((entry) => String(entry))
351
+ : [], Array.isArray(frontmatter["mandated-reading"])
352
+ ? frontmatter["mandated-reading"].map((entry) => String(entry))
353
+ : [], (type) => TYPE_TABLE.get(type)));
354
+ for (const line of outputContract) {
355
+ process.stdout.write(`${line}\n`);
356
+ }
357
+ const checks = runChecks("role-brief", decoded.value, context);
358
+ for (const line of checks.lines) {
359
+ process.stdout.write(`${line}\n`);
360
+ }
361
+ return roundTrip.lines.length > 0 || outputContract.length > 0 || checks.failed
362
+ ? 1
363
+ : 0;
364
+ }
365
+ export function cmdValidate(argv) {
366
+ const parsed = parseArgs(argv);
367
+ if (parsed.options === undefined) {
368
+ process.stderr.write(`tiphys validate: ${parsed.usageError ?? "usage error"}\n`);
369
+ process.stderr.write(`${usage()}\n`);
370
+ return EX_USAGE;
371
+ }
372
+ const { type, context, file } = parsed.options;
373
+ if (context !== undefined) {
374
+ const contextProblem = classifyContextDirectory(context);
375
+ if (contextProblem !== undefined) {
376
+ process.stderr.write(`tiphys validate: ${contextProblem}\n`);
377
+ return 1;
378
+ }
379
+ }
380
+ const read = readOperatorPath(file);
381
+ if (!read.ok) {
382
+ process.stderr.write(`tiphys validate: ${read.reason}\n`);
383
+ return 1;
384
+ }
385
+ /* M3-P5 criteria 1 and 6b. A role brief is markdown with YAML frontmatter
386
+ (section 1.5's justified exception), so it is SPLIT before it is decoded
387
+ and the schema sees the frontmatter alone. The body is not schema-parsed,
388
+ which is criterion 1's parenthetical; what it IS subject to is the clause
389
+ round trip, because criterion 6b requires deleting a clause heading to
390
+ make THIS COMMAND exit nonzero naming the orphaned id. That check reads
391
+ the include-expanded body, which it must: the two dispatch-contract
392
+ clauses exist in exactly one file and are included by all five briefs,
393
+ so a round trip that did not expand the include would report every brief
394
+ as orphaning both of them. */
395
+ if (type === "role-brief") {
396
+ return validateRoleBrief(file, read.body, context);
397
+ }
398
+ const decoded = decodeDocument(read.body, file);
399
+ if (!decoded.ok) {
400
+ process.stderr.write(`tiphys validate: ${decoded.reason}\n`);
401
+ return 1;
402
+ }
403
+ let resolvedType = type;
404
+ if (resolvedType === "auto") {
405
+ const automatic = resolveAutoType(decoded.value);
406
+ if (automatic === undefined) {
407
+ process.stderr.write(`tiphys validate: --type auto needs a kind field naming a registered type, and ${String(file)} has none\n`);
408
+ return EX_USAGE;
409
+ }
410
+ resolvedType = automatic;
411
+ }
412
+ if (!TYPE_TABLE.has(resolvedType)) {
413
+ process.stderr.write(`tiphys validate: unknown type ${resolvedType}\n`);
414
+ process.stderr.write(`${usage()}\n`);
415
+ return EX_USAGE;
416
+ }
417
+ const schema = loadTypeSchema(resolvedType);
418
+ const diagnostics = validateInstance(schema, decoded.value, companionsFor(resolvedType));
419
+ if (diagnostics.length > 0) {
420
+ for (const line of formatDiagnostics(diagnostics)) {
421
+ process.stdout.write(`${line}\n`);
422
+ }
423
+ return 1;
424
+ }
425
+ const checks = runChecks(resolvedType, decoded.value, context);
426
+ for (const line of checks.lines) {
427
+ process.stdout.write(`${line}\n`);
428
+ }
429
+ return checks.failed ? 1 : 0;
430
+ }
@@ -0,0 +1 @@
1
+ export declare function cmdWatch(args: string[]): Promise<number>;
@@ -0,0 +1,172 @@
1
+ import { EX_USAGE } from "../cli.js";
2
+ import { loadFleet } from "../fleet.js";
3
+ import { CADENCE, withCadenceOverrides } from "../liveness.js";
4
+ import { singleLine } from "../task.js";
5
+ import { NO_WAKE_EXIT, runOnce, runResident } from "../watcher.js";
6
+ /**
7
+ * tiphys watch [--once] [--interval <seconds>] [--poll <seconds>]
8
+ * [--backoff-cap <seconds>] [--max-heartbeats <n>] (kernel plan v1,
9
+ * M1-P5 step 1). Runs in a fleet home (cwd).
10
+ *
11
+ * TWO ENTRY MODES, ONE CORE (DR-0007). Without --once this is a resident
12
+ * foreground process: the caller owns it, it exits when it surfaces a
13
+ * wake, and it prints one reason line when it does. With --once it
14
+ * performs exactly one evaluation of the same wake sources and exits,
15
+ * which is what an external scheduler or a reclaimable cloud session
16
+ * uses. A pass that finds nothing actionable prints NOTHING and exits
17
+ * with the documented no-wake code 3.
18
+ *
19
+ * C-3, structurally: there is no flag here that lets a watcher outlive
20
+ * its caller, this command starts no child process, and both modes are
21
+ * ordinary foreground work. Arming is explicit and is verified through
22
+ * the beacon by the liveness guard, never by asking about a running
23
+ * program (C-2).
24
+ *
25
+ * THE CADENCE FLAGS, and who they are for. --interval sets the base
26
+ * heartbeat interval and --poll the wake-source poll interval, both in
27
+ * seconds and both accepting fractions. They exist so a harness can run
28
+ * this component at test timescales instead of waiting out production
29
+ * cadence: the M1-P6 exit-test harness is the first consumer, and it can
30
+ * replace its fixed upper bounds (a beacon within 120s, a wake within
31
+ * 180s) with a short --interval and --poll plus bounds derived from
32
+ * them. --backoff-cap sets the ceiling the doubling stops at.
33
+ * Environment variables (TIPHYS_WATCH_INTERVAL_SECONDS,
34
+ * TIPHYS_WATCH_POLL_SECONDS, TIPHYS_WATCH_BACKOFF_CAP_SECONDS,
35
+ * TIPHYS_WATCH_STALE_SECONDS) set the same values for every command,
36
+ * which is how spawn, teardown and doctor learn the same cadence: they
37
+ * carry the guard, not the watcher, so they have no flags of their own.
38
+ *
39
+ * Every effective cadence, flags included, is re-validated against the
40
+ * PR-009 invariant (stale threshold strictly greater than backoff cap
41
+ * plus one poll interval). A violation is a usage error naming both
42
+ * values, because a guard that calls a healthy watcher stale is worse
43
+ * than no guard.
44
+ */
45
+ const USAGE = "usage: tiphys watch [--once] [--interval <seconds>] [--poll <seconds>] " +
46
+ "[--backoff-cap <seconds>] [--max-heartbeats <n>]";
47
+ function usageError(message) {
48
+ if (message !== undefined) {
49
+ process.stderr.write(`tiphys watch: ${message}\n`);
50
+ }
51
+ process.stderr.write(`${USAGE}\n`);
52
+ return EX_USAGE;
53
+ }
54
+ /** A positive, finite number of seconds; anything else is a usage error. */
55
+ function positiveNumber(value) {
56
+ const parsed = Number(value);
57
+ if (!Number.isFinite(parsed) || parsed <= 0) {
58
+ return undefined;
59
+ }
60
+ return parsed;
61
+ }
62
+ function parseFlags(args) {
63
+ const parsed = {
64
+ once: false,
65
+ intervalSeconds: undefined,
66
+ pollSeconds: undefined,
67
+ backoffCapSeconds: undefined,
68
+ maxHeartbeats: undefined,
69
+ };
70
+ for (let i = 0; i < args.length; i += 1) {
71
+ const flag = args[i];
72
+ const value = args[i + 1];
73
+ if (flag === "--once") {
74
+ parsed.once = true;
75
+ }
76
+ else if (flag === "--interval" && value !== undefined) {
77
+ const seconds = positiveNumber(value);
78
+ if (seconds === undefined) {
79
+ return undefined;
80
+ }
81
+ parsed.intervalSeconds = seconds;
82
+ i += 1;
83
+ }
84
+ else if (flag === "--poll" && value !== undefined) {
85
+ const seconds = positiveNumber(value);
86
+ if (seconds === undefined) {
87
+ return undefined;
88
+ }
89
+ parsed.pollSeconds = seconds;
90
+ i += 1;
91
+ }
92
+ else if (flag === "--backoff-cap" && value !== undefined) {
93
+ const seconds = positiveNumber(value);
94
+ if (seconds === undefined) {
95
+ return undefined;
96
+ }
97
+ parsed.backoffCapSeconds = seconds;
98
+ i += 1;
99
+ }
100
+ else if (flag === "--max-heartbeats" && value !== undefined) {
101
+ const count = Number(value);
102
+ if (!Number.isInteger(count) || count <= 0) {
103
+ return undefined;
104
+ }
105
+ parsed.maxHeartbeats = count;
106
+ i += 1;
107
+ }
108
+ else {
109
+ return undefined;
110
+ }
111
+ }
112
+ return parsed;
113
+ }
114
+ function cadenceFor(flags) {
115
+ const overrides = {};
116
+ if (flags.intervalSeconds !== undefined) {
117
+ overrides.baseIntervalMs = flags.intervalSeconds * 1000;
118
+ }
119
+ if (flags.pollSeconds !== undefined) {
120
+ overrides.pollIntervalMs = flags.pollSeconds * 1000;
121
+ }
122
+ if (flags.backoffCapSeconds !== undefined) {
123
+ overrides.backoffCapMs = flags.backoffCapSeconds * 1000;
124
+ }
125
+ return withCadenceOverrides(CADENCE, overrides);
126
+ }
127
+ export async function cmdWatch(args) {
128
+ const flags = parseFlags(args);
129
+ if (flags === undefined) {
130
+ return usageError();
131
+ }
132
+ if (flags.maxHeartbeats !== undefined && flags.once) {
133
+ return usageError("--max-heartbeats bounds a resident run and has no meaning with --once");
134
+ }
135
+ let cadence;
136
+ try {
137
+ cadence = cadenceFor(flags);
138
+ }
139
+ catch (error) {
140
+ return usageError(singleLine(error.message));
141
+ }
142
+ let fleet;
143
+ try {
144
+ fleet = loadFleet(process.cwd());
145
+ }
146
+ catch (error) {
147
+ process.stderr.write(`tiphys watch: ${singleLine(error.message)}\n`);
148
+ return 1;
149
+ }
150
+ let outcome;
151
+ try {
152
+ outcome = flags.once
153
+ ? await runOnce(fleet, { cadence, maxHeartbeats: undefined })
154
+ : await runResident(fleet, { cadence, maxHeartbeats: flags.maxHeartbeats });
155
+ }
156
+ catch (error) {
157
+ // The pass wraps its own steps, so reaching here means something
158
+ // outside them raised. It is still one reason line, never a stack
159
+ // trace out of the CLI.
160
+ process.stderr.write(`tiphys watch: ${singleLine(error.message)}\n`);
161
+ return 1;
162
+ }
163
+ if (outcome.reason !== undefined) {
164
+ process.stderr.write(`tiphys watch: ${singleLine(outcome.reason)}\n`);
165
+ return outcome.code === 0 ? 1 : outcome.code;
166
+ }
167
+ if (outcome.line !== "") {
168
+ process.stdout.write(`${outcome.line}\n`);
169
+ }
170
+ // 0 with a reason line, or the documented no-wake code (NO_WAKE_EXIT).
171
+ return outcome.code;
172
+ }