@tiphys/kernel 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +611 -0
- package/LICENSE +202 -0
- package/assurance-modes.yaml +278 -0
- package/checklists/clean-room.yaml +325 -0
- package/checklists/env-failure-diagnosis.yaml +68 -0
- package/checklists/flake-playbook.yaml +68 -0
- package/checklists/hazard-review.yaml +144 -0
- package/checklists/plan-review.yaml +103 -0
- package/dist/bin/tiphys.d.ts +2 -0
- package/dist/bin/tiphys.js +14 -0
- package/dist/src/brief.d.ts +32 -0
- package/dist/src/brief.js +59 -0
- package/dist/src/checklists.d.ts +169 -0
- package/dist/src/checklists.js +310 -0
- package/dist/src/checks.d.ts +828 -0
- package/dist/src/checks.js +3314 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +57 -0
- package/dist/src/commands/brief.d.ts +92 -0
- package/dist/src/commands/brief.js +320 -0
- package/dist/src/commands/checklist.d.ts +42 -0
- package/dist/src/commands/checklist.js +168 -0
- package/dist/src/commands/doctor.d.ts +35 -0
- package/dist/src/commands/doctor.js +605 -0
- package/dist/src/commands/gates.d.ts +9 -0
- package/dist/src/commands/gates.js +360 -0
- package/dist/src/commands/init.d.ts +30 -0
- package/dist/src/commands/init.js +128 -0
- package/dist/src/commands/lock.d.ts +1 -0
- package/dist/src/commands/lock.js +229 -0
- package/dist/src/commands/mode.d.ts +40 -0
- package/dist/src/commands/mode.js +134 -0
- package/dist/src/commands/plan.d.ts +20 -0
- package/dist/src/commands/plan.js +105 -0
- package/dist/src/commands/pool.d.ts +1 -0
- package/dist/src/commands/pool.js +128 -0
- package/dist/src/commands/spawn.d.ts +1 -0
- package/dist/src/commands/spawn.js +146 -0
- package/dist/src/commands/status.d.ts +19 -0
- package/dist/src/commands/status.js +120 -0
- package/dist/src/commands/teardown.d.ts +1 -0
- package/dist/src/commands/teardown.js +79 -0
- package/dist/src/commands/tuition.d.ts +21 -0
- package/dist/src/commands/tuition.js +218 -0
- package/dist/src/commands/validate.d.ts +78 -0
- package/dist/src/commands/validate.js +360 -0
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +172 -0
- package/dist/src/exec/env.d.ts +128 -0
- package/dist/src/exec/env.js +190 -0
- package/dist/src/fleet.d.ts +51 -0
- package/dist/src/fleet.js +80 -0
- package/dist/src/gates/adapters/http-json.d.ts +5 -0
- package/dist/src/gates/adapters/http-json.js +283 -0
- package/dist/src/gates/adapters/migrations-command.d.ts +1 -0
- package/dist/src/gates/adapters/migrations-command.js +373 -0
- package/dist/src/gates/citations.d.ts +408 -0
- package/dist/src/gates/citations.js +1163 -0
- package/dist/src/gates/coverage.d.ts +284 -0
- package/dist/src/gates/coverage.js +701 -0
- package/dist/src/gates/credentials.d.ts +74 -0
- package/dist/src/gates/credentials.js +533 -0
- package/dist/src/gates/deploy.d.ts +1 -0
- package/dist/src/gates/deploy.js +33 -0
- package/dist/src/gates/manifest.d.ts +99 -0
- package/dist/src/gates/manifest.js +208 -0
- package/dist/src/gates/migrations.d.ts +1 -0
- package/dist/src/gates/migrations.js +36 -0
- package/dist/src/gates/pin.d.ts +114 -0
- package/dist/src/gates/pin.js +154 -0
- package/dist/src/gates/red-witness.d.ts +22 -0
- package/dist/src/gates/red-witness.js +390 -0
- package/dist/src/gates/release.d.ts +283 -0
- package/dist/src/gates/release.js +820 -0
- package/dist/src/gates/result.d.ts +116 -0
- package/dist/src/gates/result.js +91 -0
- package/dist/src/gates/run.d.ts +566 -0
- package/dist/src/gates/run.js +1536 -0
- package/dist/src/gates/schemas/citation-config.schema.json +59 -0
- package/dist/src/gates/schemas/coverage-config.schema.json +77 -0
- package/dist/src/gates/schemas/gate-manifest.schema.json +125 -0
- package/dist/src/gates/schemas/gate-result.schema.json +160 -0
- package/dist/src/gates/schemas/phase-declaration.schema.json +42 -0
- package/dist/src/gates/schemas/release-record.schema.json +119 -0
- package/dist/src/gates/schemas/verifier-config.schema.json +101 -0
- package/dist/src/gates/schemas/witness-spec.schema.json +110 -0
- package/dist/src/gates/scope.d.ts +131 -0
- package/dist/src/gates/scope.js +1018 -0
- package/dist/src/gates/suite.d.ts +217 -0
- package/dist/src/gates/suite.js +927 -0
- package/dist/src/gates/validate.d.ts +121 -0
- package/dist/src/gates/validate.js +414 -0
- package/dist/src/hooks.d.ts +32 -0
- package/dist/src/hooks.js +62 -0
- package/dist/src/liveness.d.ts +321 -0
- package/dist/src/liveness.js +396 -0
- package/dist/src/lock.d.ts +178 -0
- package/dist/src/lock.js +500 -0
- package/dist/src/modes.d.ts +149 -0
- package/dist/src/modes.js +258 -0
- package/dist/src/path-identity.d.ts +2 -0
- package/dist/src/path-identity.js +10 -0
- package/dist/src/plan.d.ts +73 -0
- package/dist/src/plan.js +153 -0
- package/dist/src/pool.d.ts +130 -0
- package/dist/src/pool.js +721 -0
- package/dist/src/roles.d.ts +430 -0
- package/dist/src/roles.js +734 -0
- package/dist/src/spawn.d.ts +177 -0
- package/dist/src/spawn.js +332 -0
- package/dist/src/status.d.ts +91 -0
- package/dist/src/status.js +119 -0
- package/dist/src/task.d.ts +264 -0
- package/dist/src/task.js +305 -0
- package/dist/src/teardown.d.ts +32 -0
- package/dist/src/teardown.js +314 -0
- package/dist/src/tuition.d.ts +159 -0
- package/dist/src/tuition.js +311 -0
- package/dist/src/validate.d.ts +230 -0
- package/dist/src/validate.js +732 -0
- package/dist/src/version.d.ts +3 -0
- package/dist/src/version.js +38 -0
- package/dist/src/watcher.d.ts +275 -0
- package/dist/src/watcher.js +859 -0
- package/dist/src/witness/run.d.ts +274 -0
- package/dist/src/witness/run.js +1327 -0
- package/dist/src/witness/spec.d.ts +102 -0
- package/dist/src/witness/spec.js +253 -0
- package/dist/tsconfig.src.tsbuildinfo +1 -0
- package/gate-registry.yaml +390 -0
- package/gates.manifest.json +195 -0
- package/package.json +57 -3
- package/role-model-config.yaml +88 -0
- package/roles/README.md +128 -0
- package/roles/_shared-dispatch-contract.md +87 -0
- package/roles/adversarial-plan-reviewer.md +80 -0
- package/roles/clean-room-reviewer.md +140 -0
- package/roles/implementer.md +460 -0
- package/roles/investigator.md +138 -0
- package/roles/plan-writer.md +95 -0
- package/schemas/README.md +81 -0
- package/schemas/assurance-modes.schema.json +264 -0
- package/schemas/charter.schema.json +166 -0
- package/schemas/checklist.schema.json +114 -0
- package/schemas/decision-record.schema.json +88 -0
- package/schemas/final-report.schema.json +90 -0
- package/schemas/finding.schema.json +106 -0
- package/schemas/gate-registry.schema.json +260 -0
- package/schemas/mechanism-index.schema.json +94 -0
- package/schemas/plan.schema.json +300 -0
- package/schemas/report.schema.json +579 -0
- package/schemas/role-brief.schema.json +105 -0
- package/schemas/role-model-config.schema.json +90 -0
- package/schemas/status-line.schema.json +40 -0
- package/schemas/tuition.schema.json +191 -0
- package/schemas/verdict.schema.json +289 -0
- package/schemas/work-history.schema.json +183 -0
- package/templates/charter.example.yaml +54 -0
- package/templates/decision-record.example.yaml +27 -0
- package/templates/final-report.example.yaml +80 -0
- package/templates/plan.example.yaml +87 -0
- package/templates/report.example.yaml +236 -0
- package/templates/warnings.md +74 -0
- package/templates/work-history.example.yaml +185 -0
- package/tuition/README.md +76 -0
- package/tuition/T-001.yaml +48 -0
- package/tuition/T-002.yaml +51 -0
- package/tuition/T-003.yaml +100 -0
- package/tuition/T-004.yaml +52 -0
- package/tuition/T-005.yaml +72 -0
- package/tuition/T-006.yaml +81 -0
- package/tuition/T-007.yaml +56 -0
- package/tuition/T-008.yaml +111 -0
- package/tuition/T-009.yaml +50 -0
- package/tuition/T-015.yaml +36 -0
- package/tuition/T-016.yaml +36 -0
- package/tuition/T-017.yaml +46 -0
- package/tuition/T-018.yaml +84 -0
- package/tuition/T-021.yaml +40 -0
- package/tuition/T-022.yaml +36 -0
- package/tuition/mechanism-index.yaml +256 -0
|
@@ -0,0 +1,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
|
+
}
|