@tiphys/kernel 0.1.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 (94) hide show
  1. package/AGENTS.md +56 -4
  2. package/assurance-modes.yaml +23 -2
  3. package/dist/bin/tiphys.js +86 -8
  4. package/dist/src/adapters/load.d.ts +202 -0
  5. package/dist/src/adapters/load.js +440 -0
  6. package/dist/src/brief.js +27 -20
  7. package/dist/src/checks.d.ts +720 -9
  8. package/dist/src/checks.js +1874 -163
  9. package/dist/src/cli.js +11 -0
  10. package/dist/src/commands/brief.js +27 -4
  11. package/dist/src/commands/cutover.d.ts +35 -0
  12. package/dist/src/commands/cutover.js +448 -0
  13. package/dist/src/commands/doctor.d.ts +229 -0
  14. package/dist/src/commands/doctor.js +968 -27
  15. package/dist/src/commands/init.d.ts +3 -3
  16. package/dist/src/commands/init.js +57 -8
  17. package/dist/src/commands/lock.d.ts +33 -0
  18. package/dist/src/commands/lock.js +117 -6
  19. package/dist/src/commands/next.d.ts +130 -0
  20. package/dist/src/commands/next.js +597 -0
  21. package/dist/src/commands/pool.js +12 -1
  22. package/dist/src/commands/resume.d.ts +1 -0
  23. package/dist/src/commands/resume.js +88 -0
  24. package/dist/src/commands/spawn.js +51 -2
  25. package/dist/src/commands/status.d.ts +6 -4
  26. package/dist/src/commands/status.js +6 -4
  27. package/dist/src/commands/sync.d.ts +47 -0
  28. package/dist/src/commands/sync.js +341 -0
  29. package/dist/src/commands/teardown.js +10 -2
  30. package/dist/src/commands/validate.js +70 -0
  31. package/dist/src/cutover.d.ts +584 -0
  32. package/dist/src/cutover.js +1444 -0
  33. package/dist/src/exclusion.d.ts +389 -0
  34. package/dist/src/exclusion.js +843 -0
  35. package/dist/src/exec/env.d.ts +152 -2
  36. package/dist/src/exec/env.js +146 -2
  37. package/dist/src/fleet.d.ts +172 -0
  38. package/dist/src/fleet.js +219 -1
  39. package/dist/src/gates/citations.js +7 -1
  40. package/dist/src/gates/coverage.d.ts +113 -22
  41. package/dist/src/gates/coverage.js +166 -31
  42. package/dist/src/gates/credentials.d.ts +159 -0
  43. package/dist/src/gates/credentials.js +221 -2
  44. package/dist/src/gates/gate-classes.d.ts +56 -0
  45. package/dist/src/gates/gate-classes.js +633 -0
  46. package/dist/src/gates/merge-preconditions.d.ts +319 -0
  47. package/dist/src/gates/merge-preconditions.js +932 -0
  48. package/dist/src/gates/red-witness.js +105 -13
  49. package/dist/src/gates/run.d.ts +49 -1
  50. package/dist/src/gates/run.js +83 -5
  51. package/dist/src/gates/schemas/phase-declaration.schema.json +45 -0
  52. package/dist/src/gates/suite.js +48 -7
  53. package/dist/src/hooks.d.ts +55 -3
  54. package/dist/src/hooks.js +69 -6
  55. package/dist/src/index.d.ts +31 -0
  56. package/dist/src/index.js +30 -0
  57. package/dist/src/lock.d.ts +82 -4
  58. package/dist/src/lock.js +314 -22
  59. package/dist/src/model-resolution.d.ts +159 -0
  60. package/dist/src/model-resolution.js +307 -0
  61. package/dist/src/path-identity.d.ts +32 -0
  62. package/dist/src/path-identity.js +38 -0
  63. package/dist/src/pool.d.ts +197 -1
  64. package/dist/src/pool.js +289 -22
  65. package/dist/src/roles.d.ts +31 -0
  66. package/dist/src/roles.js +42 -0
  67. package/dist/src/spawn.d.ts +307 -2
  68. package/dist/src/spawn.js +690 -19
  69. package/dist/src/status.d.ts +27 -2
  70. package/dist/src/status.js +34 -5
  71. package/dist/src/task.d.ts +295 -55
  72. package/dist/src/task.js +125 -123
  73. package/dist/src/teardown.d.ts +7 -0
  74. package/dist/src/teardown.js +120 -12
  75. package/dist/src/validate.d.ts +44 -11
  76. package/dist/src/validate.js +44 -34
  77. package/dist/src/watcher.js +1 -11
  78. package/dist/src/witness/run.d.ts +32 -7
  79. package/dist/src/witness/run.js +76 -30
  80. package/dist/src/witness/spec.d.ts +168 -0
  81. package/dist/src/witness/spec.js +240 -18
  82. package/dist/tsconfig.src.tsbuildinfo +1 -1
  83. package/gate-registry.yaml +136 -0
  84. package/gates.manifest.json +63 -1
  85. package/package.json +18 -3
  86. package/roles/implementer.md +3 -0
  87. package/schemas/README.md +1 -0
  88. package/schemas/assurance-modes.schema.json +1 -1
  89. package/schemas/charter.schema.json +19 -0
  90. package/schemas/cutover-state.schema.json +64 -0
  91. package/schemas/executor-record.schema.json +36 -0
  92. package/schemas/model-resolution.schema.json +362 -0
  93. package/schemas/verdict.schema.json +9 -3
  94. package/schemas/write-bypass.schema.json +69 -0
@@ -181,6 +181,7 @@ gates:
181
181
  paths:
182
182
  - src/
183
183
  - bin/
184
+ - plugin/
184
185
 
185
186
  # NEW IN M3-P2, entry one of three.
186
187
  - id: agent-rules-drift
@@ -281,12 +282,31 @@ gates:
281
282
  question is whether any verdict DOCUMENT exists under `delivery/review/`
282
283
  and not whether that directory does; this repository has the directory and
283
284
  has never had a verdict document in it.
285
+ THE COMMIT UNDER AUDIT IS A PARAMETER, AND ITS ABSENCE WAS THE DR-0047
286
+ SWEEP'S ONE HIGH FINDING (CR-VS-001). Without it the check grouped by the
287
+ head THE VERDICT DOCUMENTS THEMSELVES DECLARE, and nothing compared that
288
+ to the commit the gate was running against or established that it named a
289
+ commit at all. Reproduced: two decorrelated APPROVE verdicts naming
290
+ `deadbeefdeadbeefdeadbeefdeadbeefdeadbeef`, a sha `git cat-file -t`
291
+ reports as not an object, gave `check-dual-review: green`, 2 units, exit
292
+ 0, in a repository whose HEAD was a real commit and carried a further
293
+ commit of work no verdict mentioned. So ONE committed approving pair
294
+ turned this gate green for that phase at every later head, indefinitely.
295
+ The runner already had the mechanism: `scope` has declared
296
+ `parameters: [base, head]` since M2 and the runner appends the flags to
297
+ the gate command. This entry simply never declared it. With `--head` the
298
+ audited commit comes from the RUN and the declared head comes from the
299
+ EVIDENCE, and the second is measured against the first instead of
300
+ replacing it. With no `--head` the script falls back to the commit the
301
+ context's own HEAD resolves to, which is what keeps the direct workflow
302
+ step in `.github/workflows/gates.yml` correct.
284
303
  command: [node, scripts/check-dual-review.mjs, .]
285
304
  unitLabel: review verdicts examined for decorrelation
286
305
  applicability: conditional
287
306
  verified-by: script
288
307
  modes: [full, direct-pr]
289
308
  events: [pull_request]
309
+ parameters: [head]
290
310
  precondition:
291
311
  id: dual-review-verdicts-present
292
312
  kind: command-exit-zero
@@ -331,6 +351,122 @@ gates:
331
351
  modes: [full, direct-pr, local-only]
332
352
  events: [pull_request, push]
333
353
 
354
+ # NEW IN M4-P14, entry one of two (R-041, DR-0028, DR-0029, M4-P13).
355
+ - id: typecheck
356
+ $comment: >-
357
+ R-041, retargeted by M4-P13 under DR-0028. The kernel ships the gate
358
+ CLASS and no command; the project ships the command. This repository is
359
+ the project, so this is the kernel dogfooding the extension point rather
360
+ than being exempt from it (DR-0029: "the kernel becomes just another
361
+ project under the scheme, with its own registry"). The subject is the one
362
+ M4 has, package.json's build script, which CLAUDE.md already calls the
363
+ type gate. WHY IT IS NOT THE SAME THING AS THE `build` PREFLIGHT STEP:
364
+ preflight is an ordered local command with no record, no units and no
365
+ vacuity protection, so nothing downstream can tell a build that checked
366
+ the whole project from one that checked nothing. This entry produces a
367
+ GateResult whose `units` is the count of distinct paths `tsc --listFiles`
368
+ PRINTED, so a compiler that read no file is error rather than green.
369
+ WHY `--force` IS IN THE COMMAND: without it `tsc -b` is incremental and
370
+ the printed file list depends on the state of `dist/`, which would make
371
+ the unit count a property of the runner's working directory instead of
372
+ the code. `events: [pull_request]` alone, and it is derived rather than
373
+ assigned: `scripts/m2-exit-test.sh` declares the push arm's gate set once
374
+ in MAIN_ONLY_GATES and this id is not in it, so the push arm cannot run
375
+ it and test/gate-registry.test.ts asserts `events` against that harness.
376
+ command: [node, src/gates/gate-classes.ts, typecheck, --project, tsconfig.src.json, --project, tsconfig.test.json, --project, plugin/tsconfig.json]
377
+ unitLabel: source files type-checked
378
+ applicability: required
379
+ verified-by: script
380
+ modes: [full, direct-pr, local-only]
381
+ events: [pull_request]
382
+
383
+ # NEW IN M4-P14, entry two of two (DR-0029 part 2a, M4-P13).
384
+ - id: gate-classes
385
+ $comment: >-
386
+ DR-0029 part 2a made executable. A PHASE declares at least one gate in
387
+ each required class (`correctness`, `scope`, `review`), or declares an
388
+ escape that is DATA: `not-applicable` WITH a recorded reason, or
389
+ `not-yet-establishable` NAMING the phase that will establish it. The
390
+ requirement attaches to the phase and not to the project, because "this
391
+ project must have a correctness gate" is unsatisfiable on day zero while
392
+ "this phase either asserts correctness or declares when it will" is
393
+ satisfiable from the first commit. The property protected, in DR-0029's
394
+ words: you can start from nothing; you can never SILENTLY have nothing.
395
+ THE ESCAPES ARE PRINTED ON THE GREEN ARM, not merely permitted, which is
396
+ the same disclosure trade src/gates/scope.ts makes for a declaration
397
+ addition under M3-P11 change B: a reviewer who does not read the printed
398
+ line gets no protection from it. The PRECONDITION is branch-matches, the
399
+ same one `scope` carries and for the same reason: a phase declaration is
400
+ selected by phase id, and a non-phase branch has none, so the gate
401
+ reports not-applicable with an evaluated precondition rather than a
402
+ vacuous green (SC-011, M2-C-3). `events: [pull_request]` alone because
403
+ the push arm of scripts/m2-exit-test.sh passes no `--phase` at all, so
404
+ there is no declaration for it to select.
405
+ command: [node, src/gates/gate-classes.ts, gate-classes, --declarations, delivery/plan/phase-declarations, --registry, gate-registry.yaml]
406
+ unitLabel: declared gate classes checked
407
+ applicability: required
408
+ verified-by: script
409
+ modes: [full, direct-pr]
410
+ events: [pull_request]
411
+ parameters: [phase]
412
+ precondition:
413
+ id: gate-classes-branch-is-a-phase-branch
414
+ kind: branch-matches
415
+ pattern: claude/m[0-9]+-p[0-9]+-.*
416
+
417
+ # NEW IN M4-P12 (DR-0012, DR-0036, T-009, R-064, R-065a).
418
+ - id: merge-preconditions
419
+ $comment: >-
420
+ M4-P12. The artifact DR-0012's delegated grant has never had: one record
421
+ per condition, each carrying the HEAD SHA it was evaluated against, so
422
+ that "the six conditions held" stops being a sentence somebody wrote and
423
+ becomes a thing a reader can check. It is a PRECONDITION READER and not a
424
+ merge command: M4-D-09 puts the merge capability in the plugin at cutover
425
+ and DR-0036 keeps merge authority with the current process for the whole
426
+ of M4, so the orchestrator reads this record before merging, by hand.
427
+ CONDITIONS 1 AND 2 ARE COMPOSED, NOT REIMPLEMENTED. M4-P10 shipped
428
+ `dual-review-decorrelation` and `verdict-pair-approves` as derived checks
429
+ in src/checks.ts; this gate is a second caller of the same exported
430
+ primitives and runs those checks BY ID, so deregistering either one is
431
+ visible here as an `error` rather than absorbed as a pass.
432
+ `applicability: conditional`, AND THE PRECONDITION IS ANSWERED IN TWO
433
+ PLACES BECAUSE IT IS TWO QUESTIONS. The declared `command-exit-zero` block
434
+ is the SAME probe check-dual-review declares, and it answers the question
435
+ a registry precondition can answer: does this project have any committed
436
+ review verdict at all. The narrower question, whether a verdict names THIS
437
+ head, needs the run's `--head`, and a precondition command is spawned
438
+ without it (src/gates/run.ts:1524 appends the parameter flags to the GATE
439
+ command, not to the precondition's). So the gate answers that half itself
440
+ and emits its own PreconditionRecord with `met: false`, which is SC-011's
441
+ requirement that not-applicable ASSERT an evaluated precondition rather
442
+ than stand for silence. Both arms are not-applicable and each names which
443
+ question was unmet, so a reader is never told only that the gate did not
444
+ run.
445
+ `events: [pull_request]` ALONE, and it is derived rather than assigned:
446
+ scripts/m2-exit-test.sh declares the push arm's gate set once in
447
+ MAIN_ONLY_GATES and this id is not in it, so the push arm cannot run it,
448
+ and test/gate-registry.test.ts asserts `events` against that harness
449
+ rather than against a wish. It is ALSO in gates.manifest.json, because CI
450
+ invokes the runner with `--manifest`: a gate declared only here is
451
+ declared and never executed, which is a gate that cannot go red.
452
+ THE API IS PROBED, NEVER ASSUMED. CLAUDE.md standing warning 6 records
453
+ REST reachability here measured both ways on different days with the
454
+ cause unestablished, so the gate probes first and reports `error` with
455
+ units 0 when it cannot reach the API. Never `not-applicable`, never green:
456
+ M2-C-3, and the hazard this whole entry exists against is a merge
457
+ precondition check that is green because it could not look.
458
+ command: [node, src/gates/merge-preconditions.ts]
459
+ unitLabel: merge preconditions evaluated
460
+ applicability: conditional
461
+ verified-by: script
462
+ modes: [full, direct-pr]
463
+ events: [pull_request]
464
+ parameters: [head, phase]
465
+ precondition:
466
+ id: merge-preconditions-verdicts-present
467
+ kind: command-exit-zero
468
+ command: [node, scripts/check-dual-review.mjs, --precondition, .]
469
+
334
470
  # NEW IN M3-P2, entry two of three (R-043, D-11).
335
471
  - id: unit-tests-for-changed-service-methods
336
472
  $comment: >-
@@ -171,7 +171,8 @@
171
171
  "kind": "diff-touches",
172
172
  "paths": [
173
173
  "src/",
174
- "bin/"
174
+ "bin/",
175
+ "plugin/"
175
176
  ]
176
177
  }
177
178
  },
@@ -184,6 +185,67 @@
184
185
  ],
185
186
  "unitLabel": "generated brief gate rows compared",
186
187
  "applicability": "required"
188
+ },
189
+ {
190
+ "id": "typecheck",
191
+ "command": [
192
+ "node",
193
+ "src/gates/gate-classes.ts",
194
+ "typecheck",
195
+ "--project",
196
+ "tsconfig.src.json",
197
+ "--project",
198
+ "tsconfig.test.json",
199
+ "--project",
200
+ "plugin/tsconfig.json"
201
+ ],
202
+ "unitLabel": "source files type-checked",
203
+ "applicability": "required"
204
+ },
205
+ {
206
+ "id": "gate-classes",
207
+ "command": [
208
+ "node",
209
+ "src/gates/gate-classes.ts",
210
+ "gate-classes",
211
+ "--declarations",
212
+ "delivery/plan/phase-declarations",
213
+ "--registry",
214
+ "gate-registry.yaml"
215
+ ],
216
+ "unitLabel": "declared gate classes checked",
217
+ "applicability": "required",
218
+ "parameters": [
219
+ "phase"
220
+ ],
221
+ "precondition": {
222
+ "id": "gate-classes-branch-is-a-phase-branch",
223
+ "kind": "branch-matches",
224
+ "pattern": "claude/m[0-9]+-p[0-9]+-.*"
225
+ }
226
+ },
227
+ {
228
+ "id": "merge-preconditions",
229
+ "command": [
230
+ "node",
231
+ "src/gates/merge-preconditions.ts"
232
+ ],
233
+ "unitLabel": "merge preconditions evaluated",
234
+ "applicability": "conditional",
235
+ "parameters": [
236
+ "head",
237
+ "phase"
238
+ ],
239
+ "precondition": {
240
+ "id": "merge-preconditions-verdicts-present",
241
+ "kind": "command-exit-zero",
242
+ "command": [
243
+ "node",
244
+ "scripts/check-dual-review.mjs",
245
+ "--precondition",
246
+ "."
247
+ ]
248
+ }
187
249
  }
188
250
  ],
189
251
  "destructiveCommands": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiphys/kernel",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Tiphys kernel: the fleet orchestration toolbelt, delivered as an npm package.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -14,6 +14,15 @@
14
14
  "bin": {
15
15
  "tiphys": "dist/bin/tiphys.js"
16
16
  },
17
+ "main": "./dist/src/index.js",
18
+ "types": "./dist/src/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/src/index.d.ts",
22
+ "default": "./dist/src/index.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
17
26
  "files": [
18
27
  "dist",
19
28
  "!dist/node_modules",
@@ -29,10 +38,16 @@
29
38
  "templates",
30
39
  "tuition"
31
40
  ],
41
+ "workspaces": [
42
+ "plugin"
43
+ ],
44
+ "overrides": {
45
+ "@tiphys/kernel": "file:."
46
+ },
32
47
  "scripts": {
33
- "build": "tsc -b tsconfig.src.json tsconfig.test.json && npm run build:schemas && npm run build:runtime-deps",
48
+ "build": "tsc -b tsconfig.src.json tsconfig.test.json plugin/tsconfig.json && npm run build:schemas && npm run build:runtime-deps",
34
49
  "build:schemas": "node --input-type=module -e \"import { cpSync } from 'node:fs'; cpSync('src/gates/schemas', 'dist/src/gates/schemas', { recursive: true });\"",
35
- "build:runtime-deps": "node --input-type=module -e \"import { cpSync, readFileSync } from 'node:fs'; const lock = JSON.parse(readFileSync('node_modules/.package-lock.json', 'utf8')); for (const [path, entry] of Object.entries(lock.packages ?? {})) { if (!path.startsWith('node_modules/') || entry?.dev === true) continue; cpSync(path, 'dist/' + path, { recursive: true }); }\"",
50
+ "build:runtime-deps": "node --input-type=module -e \"import { cpSync, readFileSync } from 'node:fs'; const lock = JSON.parse(readFileSync('node_modules/.package-lock.json', 'utf8')); for (const [path, entry] of Object.entries(lock.packages ?? {})) { if (!path.startsWith('node_modules/') || entry?.dev === true || entry?.link === true) continue; cpSync(path, 'dist/' + path, { recursive: true }); }\"",
36
51
  "test": "node --test \"test/**/*.test.ts\"",
37
52
  "gate:suite": "node src/gates/suite.ts --pin-root src --pin-root bin --pin-root test",
38
53
  "prepack": "npm run build",
@@ -394,6 +394,9 @@ Then the gates `full` mode selects, run by `tiphys gates run --registry gate-reg
394
394
  | `check-agents-references` | script | required | references resolved |
395
395
  | `check-dual-review` | script | conditional | review verdicts examined for decorrelation |
396
396
  | `license` | script | required | production packages licensed |
397
+ | `typecheck` | script | required | source files type-checked |
398
+ | `gate-classes` | script | required | declared gate classes checked |
399
+ | `merge-preconditions` | script | conditional | merge preconditions evaluated |
397
400
  | `unit-tests-for-changed-service-methods` | clean-room-checklist (probe `unit-tests-for-changed-service-methods`) | conditional | changed service methods checked |
398
401
  | `fixtures-for-changed-component-states` | clean-room-checklist (probe `fixtures-for-changed-component-states`) | conditional | changed component states checked |
399
402
 
package/schemas/README.md CHANGED
@@ -46,6 +46,7 @@ test (`test/schemas.test.ts`).
46
46
  | `const` | |
47
47
  | `items` | |
48
48
  | `minItems` | |
49
+ | `uniqueItems` | boolean only; used by five shipped schemas here and MISSING FROM THIS TABLE from the day M3-P1 wrote both halves until the DR-0047 sweep, because nothing asserted the two agreed |
49
50
  | `minLength` | |
50
51
  | `pattern` | |
51
52
  | `$ref` | local references only; a remote reference fails compilation |
@@ -248,7 +248,7 @@
248
248
  ],
249
249
  "properties": {
250
250
  "max-fix-rounds-after-review": {
251
- "description": "How many fix rounds may follow the first dual review before the bound fires. DR-0012: \"more than two fix rounds after its first dual review\", so two.",
251
+ "description": "How many fix rounds may follow the first dual review before the bound fires. DR-0012 set two; DR-0035 replaced the single bound with a table running from one round (a zero or small subject) to three (a large subject at high impact), so what a mode records here is that table's CEILING rather than one phase's budget.",
252
252
  "type": "integer"
253
253
  },
254
254
  "recurrence-of-high-in-one-component": {
@@ -151,6 +151,25 @@
151
151
  }
152
152
  ]
153
153
  },
154
+ "review-families": {
155
+ "$comment": "DR-0038's DECLARATION, and it is a declaration rather than the exception itself (M4-D-28, kernel plan M4 section 4.2.6). DR-0012 condition 1 requires two clean-room reviews produced on different model FAMILIES. A project with one family available can satisfy that only by recording something false, and the shipped check makes it worse: it compares canonicalised STRINGS, so two models of one family compare as distinct and the pair reports decorrelated. This field is where a project states WHICH families its environment actually has. The schema does not encode the exception: single-family is `available.length === 1`, and what that then means is `dual-review-decorrelation`'s business. OPTIONAL, AND ITS ABSENCE IS NOT PERMISSION: an absent `review-families` leaves the cross-family requirement applying unchanged, so a same-family pair stays red (src/gates/release.ts:1037 states the same rule for the sibling case). WHY THE CHARTER AND NOT assurance-modes.yaml: DR-0029's split asks whether a fact is process or predicate, and 'which model families exist in this environment' is a fact about the PROJECT's environment, so it belongs to the project's own declaration; the assurance-mode vocabularies are closed by DR-0020 and bound to a derived check.",
156
+ "type": "object",
157
+ "additionalProperties": false,
158
+ "required": ["available", "reason"],
159
+ "properties": {
160
+ "available": {
161
+ "$comment": "The model families this environment can actually dispatch a clean-room review on. NOT an enum: no closed vocabulary of model families can be kept current, and a stale enum here would refuse a true declaration. The check canonicalises each entry the same way it canonicalises a verdict's `produced-by`, so the two are comparable.",
162
+ "type": "array",
163
+ "minItems": 1,
164
+ "items": { "type": "string", "minLength": 1 }
165
+ },
166
+ "reason": {
167
+ "$comment": "Investigation defence 2, the same rule release-verification mode none is held to: narrowing an owner-reserved merge condition costs a stated reason. A declaration nobody has to justify is the shape DR-0038 exists to replace.",
168
+ "type": "string",
169
+ "minLength": 1
170
+ }
171
+ }
172
+ },
154
173
  "retention": {
155
174
  "$comment": "Consumed by M3-P8 for R-098.",
156
175
  "type": "object",
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://tiphys.dev/schemas/cutover-state.schema.json",
4
+ "title": "Cutover state",
5
+ "description": "The fleet's cutover.json: the five freeze switches and what each of them records. Kernel plan M4, M4-P25 acceptance criterion 4.",
6
+ "$comment": "THE WIRE NAMES ARE camelCase AND THAT IS ESTABLISHED, NOT CHOSEN HERE. The M4 plan section spells the fields flipped-at, flipped-by, reason and restore-to; the document M4-P26 already writes and reads spells them flippedAt, flippedBy, reason and restoreTo (src/cutover.ts:98 declares the interface and src/cutover.ts:152 validates it). A schema that pinned the kebab spelling would refuse every document the shipped writer produces, so this pins what the document uses and the plan's spelling is read as prose naming the four fields rather than as their serialised keys.",
7
+ "type": "object",
8
+ "required": ["switches"],
9
+ "properties": {
10
+ "switches": {
11
+ "type": "object",
12
+ "description": "One record per switch. The five names are a CLOSED list: a sixth name is a plan revision, not a document edit, because every rollback trigger enumerates them.",
13
+ "required": [
14
+ "planning-and-scope",
15
+ "review-and-arbitration",
16
+ "credentials-and-refs",
17
+ "salvage-and-recovery",
18
+ "closeout"
19
+ ],
20
+ "properties": {
21
+ "planning-and-scope": { "$ref": "#/$defs/switchRecord" },
22
+ "review-and-arbitration": { "$ref": "#/$defs/switchRecord" },
23
+ "credentials-and-refs": { "$ref": "#/$defs/switchRecord" },
24
+ "salvage-and-recovery": { "$ref": "#/$defs/switchRecord" },
25
+ "closeout": { "$ref": "#/$defs/switchRecord" }
26
+ },
27
+ "additionalProperties": false
28
+ }
29
+ },
30
+ "additionalProperties": true,
31
+ "$defs": {
32
+ "switchRecord": {
33
+ "$comment": "additionalProperties is TRUE here and at the document level, against the schemas/README.md preference for false at every object level, and the deviation is deliberate rather than an omission. M4-P26 shipped a rollback that CARRIES document keys it does not own, at both levels, and registered that behaviour as a guarded test (test/cutover.test.ts:974). Closing either level would make the shipped writer refuse its own preserved keys, so this document is open by decision and the closed half is the switch NAME list one level up, which is the vocabulary that actually has to be closed.",
34
+ "type": "object",
35
+ "required": ["state", "flippedAt", "flippedBy", "reason", "restoreTo"],
36
+ "properties": {
37
+ "state": {
38
+ "description": "Which process holds the authority this switch governs.",
39
+ "enum": ["current", "kernel"]
40
+ },
41
+ "flippedAt": {
42
+ "description": "When the switch was last written, as an ISO 8601 instant.",
43
+ "type": "string",
44
+ "minLength": 1
45
+ },
46
+ "flippedBy": {
47
+ "description": "Who or what performed the write.",
48
+ "type": "string",
49
+ "minLength": 1
50
+ },
51
+ "reason": {
52
+ "description": "Why the switch was written.",
53
+ "type": "string",
54
+ "minLength": 1
55
+ },
56
+ "restoreTo": {
57
+ "description": "The value the switch held BEFORE the write. It is what makes rollback a matter of reading a recorded value rather than reconstructing an intent, and a record without it cannot be rolled back at all.",
58
+ "enum": ["current", "kernel"]
59
+ }
60
+ },
61
+ "additionalProperties": true
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://tiphys.dev/schemas/executor-record.schema.json",
4
+ "title": "Tiphys executor launch record",
5
+ "description": "tasks/<id>/executor.json, the record an ExecutorAdapter writes at launch (DR-0006 for the format, PR-207 for the shape, M4-P3 for the contract). Until M4-P3 this document was written by the kernel and validated by nothing, which is the state delivery/plan/m4-intake.md:272 names. It is written BEFORE the payload starts, which is what makes a failure to write it provably a launch failure and therefore safe to roll back.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["adapter", "launchedAt"],
9
+ "properties": {
10
+ "adapter": {
11
+ "$comment": "The adapter that wrote this record, by its own `name`. REQUIRED: a launch record that does not say what launched it cannot be attributed, and attribution is the whole reason an operator opens this file.",
12
+ "type": "string",
13
+ "minLength": 1
14
+ },
15
+ "launchedAt": {
16
+ "$comment": "RFC 3339 UTC, the same spelling every other Tiphys record uses, so two records from two machines sort. Written from `new Date().toISOString()`.",
17
+ "type": "string",
18
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$"
19
+ },
20
+ "deadline": {
21
+ "$comment": "PR-207. The absolute instant by which the task must have completed, present ONLY when --deadline was passed. Recorded as an instant rather than as raw seconds so the watcher compares it against the clock directly instead of re-deriving it from launchedAt.",
22
+ "type": "string",
23
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$"
24
+ },
25
+ "requestedTier": {
26
+ "$comment": "M4-P3 criterion 6. THE DECLARED TIER THE REQUEST CARRIED, VERBATIM, AND NEVER A MODEL NAME. Whatever role-model-config.yaml declares crosses the seam unaltered; the tier-to-model mapping lives in the plugin and never in src/, which is what keeps a harness that is not the one named possible at all (delivery/plan/m4-intake.md:377). The schema does not enum this value, because the vocabulary belongs to the plugin and an enum here would be that mapping's first half.",
27
+ "type": "string",
28
+ "minLength": 1
29
+ },
30
+ "requestedRole": {
31
+ "$comment": "M4-P3 criterion 6. The role the request carried, verbatim. Not enumerated here for the same reason requestedTier is not: roles/ is a shipped directory and the role-to-brief mapping is the plugin's.",
32
+ "type": "string",
33
+ "minLength": 1
34
+ }
35
+ }
36
+ }