@tiphys/kernel 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/AGENTS.md +611 -0
  2. package/LICENSE +202 -0
  3. package/assurance-modes.yaml +278 -0
  4. package/checklists/clean-room.yaml +325 -0
  5. package/checklists/env-failure-diagnosis.yaml +68 -0
  6. package/checklists/flake-playbook.yaml +68 -0
  7. package/checklists/hazard-review.yaml +144 -0
  8. package/checklists/plan-review.yaml +103 -0
  9. package/dist/bin/tiphys.d.ts +2 -0
  10. package/dist/bin/tiphys.js +14 -0
  11. package/dist/src/brief.d.ts +32 -0
  12. package/dist/src/brief.js +59 -0
  13. package/dist/src/checklists.d.ts +169 -0
  14. package/dist/src/checklists.js +310 -0
  15. package/dist/src/checks.d.ts +828 -0
  16. package/dist/src/checks.js +3314 -0
  17. package/dist/src/cli.d.ts +9 -0
  18. package/dist/src/cli.js +57 -0
  19. package/dist/src/commands/brief.d.ts +92 -0
  20. package/dist/src/commands/brief.js +320 -0
  21. package/dist/src/commands/checklist.d.ts +42 -0
  22. package/dist/src/commands/checklist.js +168 -0
  23. package/dist/src/commands/doctor.d.ts +35 -0
  24. package/dist/src/commands/doctor.js +605 -0
  25. package/dist/src/commands/gates.d.ts +9 -0
  26. package/dist/src/commands/gates.js +360 -0
  27. package/dist/src/commands/init.d.ts +30 -0
  28. package/dist/src/commands/init.js +128 -0
  29. package/dist/src/commands/lock.d.ts +1 -0
  30. package/dist/src/commands/lock.js +229 -0
  31. package/dist/src/commands/mode.d.ts +40 -0
  32. package/dist/src/commands/mode.js +134 -0
  33. package/dist/src/commands/plan.d.ts +20 -0
  34. package/dist/src/commands/plan.js +105 -0
  35. package/dist/src/commands/pool.d.ts +1 -0
  36. package/dist/src/commands/pool.js +128 -0
  37. package/dist/src/commands/spawn.d.ts +1 -0
  38. package/dist/src/commands/spawn.js +146 -0
  39. package/dist/src/commands/status.d.ts +19 -0
  40. package/dist/src/commands/status.js +120 -0
  41. package/dist/src/commands/teardown.d.ts +1 -0
  42. package/dist/src/commands/teardown.js +79 -0
  43. package/dist/src/commands/tuition.d.ts +21 -0
  44. package/dist/src/commands/tuition.js +218 -0
  45. package/dist/src/commands/validate.d.ts +78 -0
  46. package/dist/src/commands/validate.js +360 -0
  47. package/dist/src/commands/watch.d.ts +1 -0
  48. package/dist/src/commands/watch.js +172 -0
  49. package/dist/src/exec/env.d.ts +128 -0
  50. package/dist/src/exec/env.js +190 -0
  51. package/dist/src/fleet.d.ts +51 -0
  52. package/dist/src/fleet.js +80 -0
  53. package/dist/src/gates/adapters/http-json.d.ts +5 -0
  54. package/dist/src/gates/adapters/http-json.js +283 -0
  55. package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
  56. package/dist/src/gates/adapters/migrations-command.js +373 -0
  57. package/dist/src/gates/citations.d.ts +408 -0
  58. package/dist/src/gates/citations.js +1163 -0
  59. package/dist/src/gates/coverage.d.ts +284 -0
  60. package/dist/src/gates/coverage.js +701 -0
  61. package/dist/src/gates/credentials.d.ts +74 -0
  62. package/dist/src/gates/credentials.js +533 -0
  63. package/dist/src/gates/deploy.d.ts +1 -0
  64. package/dist/src/gates/deploy.js +33 -0
  65. package/dist/src/gates/manifest.d.ts +99 -0
  66. package/dist/src/gates/manifest.js +208 -0
  67. package/dist/src/gates/migrations.d.ts +1 -0
  68. package/dist/src/gates/migrations.js +36 -0
  69. package/dist/src/gates/pin.d.ts +114 -0
  70. package/dist/src/gates/pin.js +154 -0
  71. package/dist/src/gates/red-witness.d.ts +22 -0
  72. package/dist/src/gates/red-witness.js +390 -0
  73. package/dist/src/gates/release.d.ts +283 -0
  74. package/dist/src/gates/release.js +820 -0
  75. package/dist/src/gates/result.d.ts +116 -0
  76. package/dist/src/gates/result.js +91 -0
  77. package/dist/src/gates/run.d.ts +566 -0
  78. package/dist/src/gates/run.js +1536 -0
  79. package/dist/src/gates/schemas/citation-config.schema.json +59 -0
  80. package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
  81. package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
  82. package/dist/src/gates/schemas/gate-result.schema.json +160 -0
  83. package/dist/src/gates/schemas/phase-declaration.schema.json +42 -0
  84. package/dist/src/gates/schemas/release-record.schema.json +119 -0
  85. package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
  86. package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
  87. package/dist/src/gates/scope.d.ts +131 -0
  88. package/dist/src/gates/scope.js +1018 -0
  89. package/dist/src/gates/suite.d.ts +217 -0
  90. package/dist/src/gates/suite.js +927 -0
  91. package/dist/src/gates/validate.d.ts +121 -0
  92. package/dist/src/gates/validate.js +414 -0
  93. package/dist/src/hooks.d.ts +32 -0
  94. package/dist/src/hooks.js +62 -0
  95. package/dist/src/liveness.d.ts +321 -0
  96. package/dist/src/liveness.js +396 -0
  97. package/dist/src/lock.d.ts +178 -0
  98. package/dist/src/lock.js +500 -0
  99. package/dist/src/modes.d.ts +149 -0
  100. package/dist/src/modes.js +258 -0
  101. package/dist/src/path-identity.d.ts +2 -0
  102. package/dist/src/path-identity.js +10 -0
  103. package/dist/src/plan.d.ts +73 -0
  104. package/dist/src/plan.js +153 -0
  105. package/dist/src/pool.d.ts +130 -0
  106. package/dist/src/pool.js +721 -0
  107. package/dist/src/roles.d.ts +430 -0
  108. package/dist/src/roles.js +734 -0
  109. package/dist/src/spawn.d.ts +177 -0
  110. package/dist/src/spawn.js +332 -0
  111. package/dist/src/status.d.ts +91 -0
  112. package/dist/src/status.js +119 -0
  113. package/dist/src/task.d.ts +264 -0
  114. package/dist/src/task.js +305 -0
  115. package/dist/src/teardown.d.ts +32 -0
  116. package/dist/src/teardown.js +314 -0
  117. package/dist/src/tuition.d.ts +159 -0
  118. package/dist/src/tuition.js +311 -0
  119. package/dist/src/validate.d.ts +230 -0
  120. package/dist/src/validate.js +732 -0
  121. package/dist/src/version.d.ts +3 -0
  122. package/dist/src/version.js +38 -0
  123. package/dist/src/watcher.d.ts +275 -0
  124. package/dist/src/watcher.js +859 -0
  125. package/dist/src/witness/run.d.ts +274 -0
  126. package/dist/src/witness/run.js +1327 -0
  127. package/dist/src/witness/spec.d.ts +102 -0
  128. package/dist/src/witness/spec.js +253 -0
  129. package/dist/tsconfig.src.tsbuildinfo +1 -0
  130. package/gate-registry.yaml +390 -0
  131. package/gates.manifest.json +195 -0
  132. package/package.json +57 -3
  133. package/role-model-config.yaml +88 -0
  134. package/roles/README.md +128 -0
  135. package/roles/_shared-dispatch-contract.md +87 -0
  136. package/roles/adversarial-plan-reviewer.md +80 -0
  137. package/roles/clean-room-reviewer.md +140 -0
  138. package/roles/implementer.md +460 -0
  139. package/roles/investigator.md +138 -0
  140. package/roles/plan-writer.md +95 -0
  141. package/schemas/README.md +81 -0
  142. package/schemas/assurance-modes.schema.json +264 -0
  143. package/schemas/charter.schema.json +166 -0
  144. package/schemas/checklist.schema.json +114 -0
  145. package/schemas/decision-record.schema.json +88 -0
  146. package/schemas/final-report.schema.json +90 -0
  147. package/schemas/finding.schema.json +106 -0
  148. package/schemas/gate-registry.schema.json +260 -0
  149. package/schemas/mechanism-index.schema.json +94 -0
  150. package/schemas/plan.schema.json +300 -0
  151. package/schemas/report.schema.json +579 -0
  152. package/schemas/role-brief.schema.json +105 -0
  153. package/schemas/role-model-config.schema.json +90 -0
  154. package/schemas/status-line.schema.json +40 -0
  155. package/schemas/tuition.schema.json +191 -0
  156. package/schemas/verdict.schema.json +289 -0
  157. package/schemas/work-history.schema.json +183 -0
  158. package/templates/charter.example.yaml +54 -0
  159. package/templates/decision-record.example.yaml +27 -0
  160. package/templates/final-report.example.yaml +80 -0
  161. package/templates/plan.example.yaml +87 -0
  162. package/templates/report.example.yaml +236 -0
  163. package/templates/warnings.md +74 -0
  164. package/templates/work-history.example.yaml +185 -0
  165. package/tuition/README.md +76 -0
  166. package/tuition/T-001.yaml +48 -0
  167. package/tuition/T-002.yaml +51 -0
  168. package/tuition/T-003.yaml +100 -0
  169. package/tuition/T-004.yaml +52 -0
  170. package/tuition/T-005.yaml +72 -0
  171. package/tuition/T-006.yaml +81 -0
  172. package/tuition/T-007.yaml +56 -0
  173. package/tuition/T-008.yaml +111 -0
  174. package/tuition/T-009.yaml +50 -0
  175. package/tuition/T-015.yaml +36 -0
  176. package/tuition/T-016.yaml +36 -0
  177. package/tuition/T-017.yaml +46 -0
  178. package/tuition/T-018.yaml +84 -0
  179. package/tuition/T-021.yaml +40 -0
  180. package/tuition/T-022.yaml +36 -0
  181. package/tuition/mechanism-index.yaml +256 -0
@@ -0,0 +1,78 @@
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 type { SchemaDocument } from "../validate.ts";
26
+ /** Exit code for usage errors, per BSD sysexits EX_USAGE. */
27
+ export declare const EX_USAGE = 64;
28
+ /**
29
+ * The artifact types this milestone's validator knows, and the schema
30
+ * document each resolves to.
31
+ *
32
+ * REGISTERED HERE, EXTENDED BY EVERY LATER PHASE (M3R-001). A phase that
33
+ * introduces an artifact type adds its row here and to the `auto` resolver in
34
+ * the same step, so a type whose schema ships but which `--type` cannot name
35
+ * is not a state this command can be in.
36
+ */
37
+ export declare const TYPE_TABLE: ReadonlyMap<string, string>;
38
+ /**
39
+ * COMPANION SCHEMAS, declared beside the type table (M3-P4 step 3).
40
+ *
41
+ * A type listed here is compiled with the named sibling documents registered
42
+ * alongside it, so a `$ref` that leaves the document resolves. THE SET IS
43
+ * DECLARED, NOT DISCOVERED: `src/validate.ts` never reads a reference and
44
+ * fetches what it names, so a `$ref` to a document absent from this table
45
+ * still fails compilation with `unresolvedRef`. That is the property DR-0013
46
+ * clause 4 protects, kept while letting two artifact types share ONE
47
+ * definition of the honesty contract instead of two that can drift.
48
+ *
49
+ * The reference in `work-history.schema.json` is RELATIVE
50
+ * (`report.schema.json#/$defs/claim`), so it resolves against that document's
51
+ * `$id` to `https://tiphys.dev/schemas/report.schema.json`, which is exactly
52
+ * the companion's own `$id`.
53
+ */
54
+ export declare const COMPANION_TABLE: ReadonlyMap<string, readonly string[]>;
55
+ /** The companion schema documents a type is compiled with, in declared order. */
56
+ export declare function companionsFor(type: string): SchemaDocument[];
57
+ /**
58
+ * Locate the shipped `schemas/` directory by walking UP from this module.
59
+ *
60
+ * The depth differs between the two layouts this code runs in: from source it
61
+ * is `src/commands/` and the directory is two levels up, and from the built
62
+ * entry it is `dist/src/commands/` and the directory is three levels up
63
+ * beside the package root. Counting `..` would therefore be right in exactly
64
+ * one of them, which is the kind of silent, layout-dependent break M2's own
65
+ * schema resolution comment warns about. Walking up and TESTING for the
66
+ * directory is right in both, and in a relocated copy as well.
67
+ */
68
+ export declare function schemasDirectory(): string;
69
+ /** Read and decode a shipped schema document. */
70
+ export declare function loadTypeSchema(type: string): SchemaDocument;
71
+ /**
72
+ * Resolve `--type auto` from the instance's `kind` field. An instance with no
73
+ * `kind`, or with a `kind` no schema is registered for, is a USAGE error: the
74
+ * caller asked the command to work out which contract applies and it cannot,
75
+ * which is not the same as the document failing that contract.
76
+ */
77
+ export declare function resolveAutoType(instance: unknown): string | undefined;
78
+ export declare function cmdValidate(argv: string[]): number;
@@ -0,0 +1,360 @@
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
+ ]);
100
+ /**
101
+ * COMPANION SCHEMAS, declared beside the type table (M3-P4 step 3).
102
+ *
103
+ * A type listed here is compiled with the named sibling documents registered
104
+ * alongside it, so a `$ref` that leaves the document resolves. THE SET IS
105
+ * DECLARED, NOT DISCOVERED: `src/validate.ts` never reads a reference and
106
+ * fetches what it names, so a `$ref` to a document absent from this table
107
+ * still fails compilation with `unresolvedRef`. That is the property DR-0013
108
+ * clause 4 protects, kept while letting two artifact types share ONE
109
+ * definition of the honesty contract instead of two that can drift.
110
+ *
111
+ * The reference in `work-history.schema.json` is RELATIVE
112
+ * (`report.schema.json#/$defs/claim`), so it resolves against that document's
113
+ * `$id` to `https://tiphys.dev/schemas/report.schema.json`, which is exactly
114
+ * the companion's own `$id`.
115
+ */
116
+ export const COMPANION_TABLE = new Map([
117
+ ["work-history", ["report"]],
118
+ ]);
119
+ /** The companion schema documents a type is compiled with, in declared order. */
120
+ export function companionsFor(type) {
121
+ return (COMPANION_TABLE.get(type) ?? []).map((companion) => loadTypeSchema(companion));
122
+ }
123
+ /**
124
+ * Locate the shipped `schemas/` directory by walking UP from this module.
125
+ *
126
+ * The depth differs between the two layouts this code runs in: from source it
127
+ * is `src/commands/` and the directory is two levels up, and from the built
128
+ * entry it is `dist/src/commands/` and the directory is three levels up
129
+ * beside the package root. Counting `..` would therefore be right in exactly
130
+ * one of them, which is the kind of silent, layout-dependent break M2's own
131
+ * schema resolution comment warns about. Walking up and TESTING for the
132
+ * directory is right in both, and in a relocated copy as well.
133
+ */
134
+ export function schemasDirectory() {
135
+ let directory = dirname(fileURLToPath(import.meta.url));
136
+ for (let depth = 0; depth < 8; depth += 1) {
137
+ const candidate = join(directory, "schemas");
138
+ try {
139
+ const entries = readdirSync(candidate);
140
+ if (entries.some((name) => name.endsWith(".schema.json"))) {
141
+ return candidate;
142
+ }
143
+ }
144
+ catch {
145
+ /* not here; keep walking */
146
+ }
147
+ const parent = dirname(directory);
148
+ if (parent === directory) {
149
+ break;
150
+ }
151
+ directory = parent;
152
+ }
153
+ throw new Error("the shipped schemas/ directory was not found above this module; the installation is incomplete");
154
+ }
155
+ const schemaCache = new Map();
156
+ /** Read and decode a shipped schema document. */
157
+ export function loadTypeSchema(type) {
158
+ const cached = schemaCache.get(type);
159
+ if (cached !== undefined) {
160
+ return cached;
161
+ }
162
+ const filename = TYPE_TABLE.get(type);
163
+ if (filename === undefined) {
164
+ throw new Error(`no schema is registered for type ${type}`);
165
+ }
166
+ const path = join(schemasDirectory(), filename);
167
+ const read = readOperatorPath(path);
168
+ if (!read.ok) {
169
+ throw new Error(read.reason);
170
+ }
171
+ const decoded = decodeDocument(read.body, path);
172
+ if (!decoded.ok) {
173
+ throw new Error(decoded.reason);
174
+ }
175
+ const document = decoded.value;
176
+ schemaCache.set(type, document);
177
+ return document;
178
+ }
179
+ /**
180
+ * Resolve `--type auto` from the instance's `kind` field. An instance with no
181
+ * `kind`, or with a `kind` no schema is registered for, is a USAGE error: the
182
+ * caller asked the command to work out which contract applies and it cannot,
183
+ * which is not the same as the document failing that contract.
184
+ */
185
+ export function resolveAutoType(instance) {
186
+ if (typeof instance !== "object" || instance === null || Array.isArray(instance)) {
187
+ return undefined;
188
+ }
189
+ const kind = instance["kind"];
190
+ if (typeof kind !== "string" || !TYPE_TABLE.has(kind)) {
191
+ return undefined;
192
+ }
193
+ return kind;
194
+ }
195
+ function usage() {
196
+ const types = [...TYPE_TABLE.keys()].sort().join(" | ");
197
+ return `usage: tiphys validate --type <${types} | auto> [--context <dir>] <file>`;
198
+ }
199
+ function parseArgs(argv) {
200
+ const options = {};
201
+ for (let index = 0; index < argv.length; index += 1) {
202
+ const argument = argv[index];
203
+ if (argument === "--type" || argument === "--context") {
204
+ const value = argv[index + 1];
205
+ if (value === undefined || value.startsWith("--")) {
206
+ return { usageError: `${argument} requires a value` };
207
+ }
208
+ if (argument === "--type") {
209
+ options.type = value;
210
+ }
211
+ else {
212
+ options.context = value;
213
+ }
214
+ index += 1;
215
+ continue;
216
+ }
217
+ if (argument.startsWith("--")) {
218
+ return { usageError: `unknown option ${argument}` };
219
+ }
220
+ if (options.file !== undefined) {
221
+ return { usageError: "exactly one file argument is accepted" };
222
+ }
223
+ options.file = argument;
224
+ }
225
+ if (options.type === undefined) {
226
+ return { usageError: "--type is required" };
227
+ }
228
+ if (options.file === undefined) {
229
+ return { usageError: "a file argument is required" };
230
+ }
231
+ return { options };
232
+ }
233
+ /**
234
+ * Validate one role brief: frontmatter against `role-brief.schema.json`, then
235
+ * the clause round trip over the include-expanded body (M3-P5 criteria 1 and
236
+ * 6b). Kept as its own function rather than folded into `cmdValidate`'s main
237
+ * path, because a role brief is the one artifact type whose file is not a
238
+ * YAML document and conflating the two decode paths is how a horizontal rule
239
+ * becomes a contract.
240
+ */
241
+ function validateRoleBrief(file, body, context) {
242
+ const split = splitFrontmatter(body, file);
243
+ if (!split.ok) {
244
+ process.stderr.write(`tiphys validate: ${split.reason}\n`);
245
+ return 1;
246
+ }
247
+ const decoded = decodeDocument(split.frontmatter, `${file} frontmatter`);
248
+ if (!decoded.ok) {
249
+ process.stderr.write(`tiphys validate: ${decoded.reason}\n`);
250
+ return 1;
251
+ }
252
+ const schema = loadTypeSchema("role-brief");
253
+ const diagnostics = validateInstance(schema, decoded.value);
254
+ if (diagnostics.length > 0) {
255
+ for (const line of formatDiagnostics(diagnostics)) {
256
+ process.stdout.write(`${line}\n`);
257
+ }
258
+ return 1;
259
+ }
260
+ const frontmatter = decoded.value;
261
+ const clauses = Array.isArray(frontmatter["clauses"])
262
+ ? frontmatter["clauses"].map((entry) => String(entry))
263
+ : [];
264
+ const roundTrip = roleBriefBodyDiagnostics(file, split.body, clauses);
265
+ if (!roundTrip.ok) {
266
+ process.stderr.write(`tiphys validate: ${roundTrip.reason}\n`);
267
+ return 1;
268
+ }
269
+ for (const line of roundTrip.lines) {
270
+ process.stdout.write(`${line}\n`);
271
+ }
272
+ /* THE OUTPUT CONTRACT (M3-P5 fix round 1). Every declared output type whose
273
+ schema is registered must have that schema on mandated-reading. Wired
274
+ HERE, and the reason is that TYPE_TABLE is the map and it lives in this
275
+ module: passing the lookup in as a function keeps src/roles.ts free of an
276
+ import back into this command, so the two do not become a cycle. The
277
+ lookup is the SAME table `--type` resolves against, so the check cannot
278
+ drift from what the validator would actually compile. */
279
+ const outputContract = formatDiagnostics(outputContractDiagnostics(Array.isArray(frontmatter["outputs"])
280
+ ? frontmatter["outputs"].map((entry) => String(entry))
281
+ : [], Array.isArray(frontmatter["mandated-reading"])
282
+ ? frontmatter["mandated-reading"].map((entry) => String(entry))
283
+ : [], (type) => TYPE_TABLE.get(type)));
284
+ for (const line of outputContract) {
285
+ process.stdout.write(`${line}\n`);
286
+ }
287
+ const checks = runChecks("role-brief", decoded.value, context);
288
+ for (const line of checks.lines) {
289
+ process.stdout.write(`${line}\n`);
290
+ }
291
+ return roundTrip.lines.length > 0 || outputContract.length > 0 || checks.failed
292
+ ? 1
293
+ : 0;
294
+ }
295
+ export function cmdValidate(argv) {
296
+ const parsed = parseArgs(argv);
297
+ if (parsed.options === undefined) {
298
+ process.stderr.write(`tiphys validate: ${parsed.usageError ?? "usage error"}\n`);
299
+ process.stderr.write(`${usage()}\n`);
300
+ return EX_USAGE;
301
+ }
302
+ const { type, context, file } = parsed.options;
303
+ if (context !== undefined) {
304
+ const contextProblem = classifyContextDirectory(context);
305
+ if (contextProblem !== undefined) {
306
+ process.stderr.write(`tiphys validate: ${contextProblem}\n`);
307
+ return 1;
308
+ }
309
+ }
310
+ const read = readOperatorPath(file);
311
+ if (!read.ok) {
312
+ process.stderr.write(`tiphys validate: ${read.reason}\n`);
313
+ return 1;
314
+ }
315
+ /* M3-P5 criteria 1 and 6b. A role brief is markdown with YAML frontmatter
316
+ (section 1.5's justified exception), so it is SPLIT before it is decoded
317
+ and the schema sees the frontmatter alone. The body is not schema-parsed,
318
+ which is criterion 1's parenthetical; what it IS subject to is the clause
319
+ round trip, because criterion 6b requires deleting a clause heading to
320
+ make THIS COMMAND exit nonzero naming the orphaned id. That check reads
321
+ the include-expanded body, which it must: the two dispatch-contract
322
+ clauses exist in exactly one file and are included by all five briefs,
323
+ so a round trip that did not expand the include would report every brief
324
+ as orphaning both of them. */
325
+ if (type === "role-brief") {
326
+ return validateRoleBrief(file, read.body, context);
327
+ }
328
+ const decoded = decodeDocument(read.body, file);
329
+ if (!decoded.ok) {
330
+ process.stderr.write(`tiphys validate: ${decoded.reason}\n`);
331
+ return 1;
332
+ }
333
+ let resolvedType = type;
334
+ if (resolvedType === "auto") {
335
+ const automatic = resolveAutoType(decoded.value);
336
+ if (automatic === undefined) {
337
+ process.stderr.write(`tiphys validate: --type auto needs a kind field naming a registered type, and ${String(file)} has none\n`);
338
+ return EX_USAGE;
339
+ }
340
+ resolvedType = automatic;
341
+ }
342
+ if (!TYPE_TABLE.has(resolvedType)) {
343
+ process.stderr.write(`tiphys validate: unknown type ${resolvedType}\n`);
344
+ process.stderr.write(`${usage()}\n`);
345
+ return EX_USAGE;
346
+ }
347
+ const schema = loadTypeSchema(resolvedType);
348
+ const diagnostics = validateInstance(schema, decoded.value, companionsFor(resolvedType));
349
+ if (diagnostics.length > 0) {
350
+ for (const line of formatDiagnostics(diagnostics)) {
351
+ process.stdout.write(`${line}\n`);
352
+ }
353
+ return 1;
354
+ }
355
+ const checks = runChecks(resolvedType, decoded.value, context);
356
+ for (const line of checks.lines) {
357
+ process.stdout.write(`${line}\n`);
358
+ }
359
+ return checks.failed ? 1 : 0;
360
+ }
@@ -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
+ }