@codyswann/lisa 2.345.1 → 2.348.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 (139) hide show
  1. package/all/copy-overwrite/scripts/check-state-classification.mjs +609 -0
  2. package/all/copy-overwrite/scripts/lisa-command-envelope.mjs +194 -0
  3. package/all/copy-overwrite/scripts/lisa-schema-validate.mjs +208 -0
  4. package/all/copy-overwrite/scripts/schemas/lisa-command-envelope.v1.schema.json +119 -0
  5. package/all/copy-overwrite/scripts/schemas/lisa-state-contract.v1.schema.json +191 -0
  6. package/all/create-only/state/README.md +41 -0
  7. package/all/create-only/state/state-contract.example.json +46 -0
  8. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  9. package/dist/core/upstream-evidence-manifest.js +92 -10
  10. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  11. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +224 -0
  12. package/expo/copy-overwrite/scripts/bdd/contract.mjs +121 -0
  13. package/expo/copy-overwrite/scripts/bdd/envelope.mjs +231 -0
  14. package/expo/copy-overwrite/scripts/bdd/parse.mjs +220 -0
  15. package/expo/copy-overwrite/scripts/bdd/render.mjs +170 -0
  16. package/expo/copy-overwrite/scripts/bdd/report.mjs +361 -0
  17. package/expo/copy-overwrite/scripts/bdd/validate.mjs +282 -0
  18. package/expo/copy-overwrite/scripts/bdd/waivers.mjs +210 -0
  19. package/expo/copy-overwrite/scripts/bdd-matrix.mjs +278 -0
  20. package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +657 -0
  21. package/expo/create-only/bdd/coverage-map.json +38 -0
  22. package/expo/create-only/bdd/features/.keep +5 -0
  23. package/expo/github-rulesets/bdd-coverage.json +33 -0
  24. package/expo/package-lisa/package.lisa.json +3 -0
  25. package/package.json +1 -1
  26. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa/.codex-plugin/skills/lisa-acceptance-criteria/SKILL.md +1 -0
  29. package/plugins/lisa/.codex-plugin/skills/lisa-codify-verification/SKILL.md +4 -0
  30. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +2 -0
  31. package/plugins/lisa/.codex-plugin/skills/lisa-research/SKILL.md +7 -1
  32. package/plugins/lisa/.codex-plugin/skills/lisa-task-decomposition/SKILL.md +2 -0
  33. package/plugins/lisa/.codex-plugin/skills/lisa-test-strategy/SKILL.md +1 -0
  34. package/plugins/lisa/.codex-plugin/skills/lisa-verification-lifecycle/SKILL.md +1 -0
  35. package/plugins/lisa/.codex-plugin/skills/lisa-verify/SKILL.md +1 -1
  36. package/plugins/lisa/rules/eager/reset-seed-coverage.md +48 -0
  37. package/plugins/lisa/rules/reference/reset-seed-coverage.md +231 -0
  38. package/plugins/lisa/skills/lisa-acceptance-criteria/SKILL.md +1 -0
  39. package/plugins/lisa/skills/lisa-codify-verification/SKILL.md +4 -0
  40. package/plugins/lisa/skills/lisa-implement/SKILL.md +2 -0
  41. package/plugins/lisa/skills/lisa-research/SKILL.md +7 -1
  42. package/plugins/lisa/skills/lisa-task-decomposition/SKILL.md +2 -0
  43. package/plugins/lisa/skills/lisa-test-strategy/SKILL.md +1 -0
  44. package/plugins/lisa/skills/lisa-verification-lifecycle/SKILL.md +1 -0
  45. package/plugins/lisa/skills/lisa-verify/SKILL.md +1 -1
  46. package/plugins/lisa-agy/plugin.json +1 -1
  47. package/plugins/lisa-agy/skills/lisa-acceptance-criteria/SKILL.md +1 -0
  48. package/plugins/lisa-agy/skills/lisa-codify-verification/SKILL.md +4 -0
  49. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +2 -0
  50. package/plugins/lisa-agy/skills/lisa-research/SKILL.md +7 -1
  51. package/plugins/lisa-agy/skills/lisa-task-decomposition/SKILL.md +2 -0
  52. package/plugins/lisa-agy/skills/lisa-test-strategy/SKILL.md +1 -0
  53. package/plugins/lisa-agy/skills/lisa-verification-lifecycle/SKILL.md +1 -0
  54. package/plugins/lisa-agy/skills/lisa-verify/SKILL.md +1 -1
  55. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  58. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-copilot/rules/eager/reset-seed-coverage.md +48 -0
  62. package/plugins/lisa-copilot/rules/reference/reset-seed-coverage.md +231 -0
  63. package/plugins/lisa-copilot/skills/lisa-acceptance-criteria/SKILL.md +1 -0
  64. package/plugins/lisa-copilot/skills/lisa-codify-verification/SKILL.md +4 -0
  65. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +2 -0
  66. package/plugins/lisa-copilot/skills/lisa-research/SKILL.md +7 -1
  67. package/plugins/lisa-copilot/skills/lisa-task-decomposition/SKILL.md +2 -0
  68. package/plugins/lisa-copilot/skills/lisa-test-strategy/SKILL.md +1 -0
  69. package/plugins/lisa-copilot/skills/lisa-verification-lifecycle/SKILL.md +1 -0
  70. package/plugins/lisa-copilot/skills/lisa-verify/SKILL.md +1 -1
  71. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-cursor/rules/reset-seed-coverage-reference.mdc +236 -0
  73. package/plugins/lisa-cursor/rules/reset-seed-coverage.mdc +53 -0
  74. package/plugins/lisa-cursor/skills/lisa-acceptance-criteria/SKILL.md +1 -0
  75. package/plugins/lisa-cursor/skills/lisa-codify-verification/SKILL.md +4 -0
  76. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +2 -0
  77. package/plugins/lisa-cursor/skills/lisa-research/SKILL.md +7 -1
  78. package/plugins/lisa-cursor/skills/lisa-task-decomposition/SKILL.md +2 -0
  79. package/plugins/lisa-cursor/skills/lisa-test-strategy/SKILL.md +1 -0
  80. package/plugins/lisa-cursor/skills/lisa-verification-lifecycle/SKILL.md +1 -0
  81. package/plugins/lisa-cursor/skills/lisa-verify/SKILL.md +1 -1
  82. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  84. package/plugins/lisa-expo/.codex-plugin/skills/ops-db-ops/SKILL.md +93 -2
  85. package/plugins/lisa-expo/skills/ops-db-ops/SKILL.md +94 -3
  86. package/plugins/lisa-expo/skills/ops-db-ops/agents/openai.yaml +2 -2
  87. package/plugins/lisa-expo-agy/plugin.json +1 -1
  88. package/plugins/lisa-expo-agy/skills/ops-db-ops/SKILL.md +94 -3
  89. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-expo-copilot/skills/ops-db-ops/SKILL.md +94 -3
  91. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-expo-cursor/skills/ops-db-ops/SKILL.md +94 -3
  93. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  96. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  100. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  101. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  106. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  111. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-rails-agy/plugin.json +1 -1
  116. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  120. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  121. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  125. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  126. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  128. package/plugins/src/base/rules/eager/reset-seed-coverage.md +48 -0
  129. package/plugins/src/base/rules/reference/reset-seed-coverage.md +231 -0
  130. package/plugins/src/base/skills/lisa-acceptance-criteria/SKILL.md +1 -0
  131. package/plugins/src/base/skills/lisa-codify-verification/SKILL.md +4 -0
  132. package/plugins/src/base/skills/lisa-implement/SKILL.md +2 -0
  133. package/plugins/src/base/skills/lisa-research/SKILL.md +7 -1
  134. package/plugins/src/base/skills/lisa-task-decomposition/SKILL.md +2 -0
  135. package/plugins/src/base/skills/lisa-test-strategy/SKILL.md +1 -0
  136. package/plugins/src/base/skills/lisa-verification-lifecycle/SKILL.md +1 -0
  137. package/plugins/src/base/skills/lisa-verify/SKILL.md +1 -1
  138. package/plugins/src/expo/skills/ops-db-ops/SKILL.md +94 -3
  139. package/scripts/check-state-classification.mjs +27 -0
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lisa-command-envelope — the standard result every capability adapter returns.
4
+ *
5
+ * Governed by the `reset-seed-coverage` rule. One validated JSON object on
6
+ * stdout, human narration on stderr, and an exit code that means exactly one
7
+ * thing: **0 = the operation completed AND verified**. Every other outcome is
8
+ * nonzero and says which in `status`.
9
+ *
10
+ * The point of the envelope is that "every repo answers the same interface the
11
+ * same way" becomes checkable. In particular a declared noop is machine-readable
12
+ * (`mode: "declared-noop"` plus reason, owner and capability-manifest
13
+ * reference) — a bare `exit 0` with no output is indistinguishable from a
14
+ * successful destructive run and is never an acceptable answer.
15
+ *
16
+ * CLI:
17
+ * node scripts/lisa-command-envelope.mjs --print-schema
18
+ * node scripts/lisa-command-envelope.mjs --validate <file> # "-" reads stdin
19
+ * @module scripts/lisa-command-envelope
20
+ */
21
+ import * as fs from "node:fs";
22
+ import * as path from "node:path";
23
+ import { fileURLToPath, pathToFileURL } from "node:url";
24
+
25
+ import { validateAgainstSchema } from "./lisa-schema-validate.mjs";
26
+
27
+ /** Pinned envelope schema version. */
28
+ export const COMMAND_ENVELOPE_SCHEMA_VERSION = "lisa-command-envelope-v1";
29
+
30
+ const SCHEMA_PATH = path.join(
31
+ path.dirname(fileURLToPath(import.meta.url)),
32
+ "schemas",
33
+ "lisa-command-envelope.v1.schema.json"
34
+ );
35
+
36
+ /** The published envelope schema, read from the shipped JSON document. */
37
+ export const COMMAND_ENVELOPE_SCHEMA = JSON.parse(
38
+ fs.readFileSync(SCHEMA_PATH, "utf8")
39
+ );
40
+
41
+ /**
42
+ * Statuses that may exit 0.
43
+ *
44
+ * `detection-only` is deliberately NOT here: it means the adapter could not
45
+ * observe authoritative state, so its findings are non-authoritative, and exit
46
+ * 0 must never mean "I could not look."
47
+ */
48
+ export const SUCCESS_STATUSES = Object.freeze([
49
+ "completed",
50
+ "no-op",
51
+ "not-adopted",
52
+ ]);
53
+
54
+ /**
55
+ * Exit code implied by a status.
56
+ * @param {string} status - Envelope status
57
+ * @returns {number} 0 when the operation completed and verified, else 1
58
+ */
59
+ export function exitCodeForStatus(status) {
60
+ return SUCCESS_STATUSES.includes(status) ? 0 : 1;
61
+ }
62
+
63
+ /**
64
+ * Validate an envelope against the published schema, plus the conditional
65
+ * requirements the schema alone cannot express.
66
+ * @param {unknown} envelope - Candidate envelope
67
+ * @returns {{ valid: boolean, errors: string[] }} Validation outcome
68
+ */
69
+ export function validateEnvelope(envelope) {
70
+ const base = validateAgainstSchema(envelope, COMMAND_ENVELOPE_SCHEMA);
71
+ if (!base.valid) {
72
+ return base;
73
+ }
74
+ const errors = [];
75
+ const declarative =
76
+ envelope.mode === "declared-noop" || envelope.mode === "not-applicable";
77
+ if (declarative) {
78
+ for (const field of ["reason", "owner", "capabilityManifest"]) {
79
+ if (!envelope[field]) {
80
+ errors.push(
81
+ `mode "${envelope.mode}" requires "${field}" — a bare success is indistinguishable from a real run`
82
+ );
83
+ }
84
+ }
85
+ }
86
+ if (!SUCCESS_STATUSES.includes(envelope.status) && !envelope.reason) {
87
+ errors.push(`status "${envelope.status}" requires "reason"`);
88
+ }
89
+ if (envelope.mode === "declared-noop" && envelope.status !== "no-op") {
90
+ errors.push('mode "declared-noop" requires status "no-op"');
91
+ }
92
+ return { valid: errors.length === 0, errors };
93
+ }
94
+
95
+ /**
96
+ * Build a validated envelope, filling in the invariant fields.
97
+ * @param {object} fields - Envelope fields (see the published schema)
98
+ * @returns {object} A frozen, validated envelope
99
+ * @throws {Error} When the resulting envelope is invalid
100
+ */
101
+ export function buildEnvelope(fields) {
102
+ const { summary = {}, ...rest } = fields;
103
+ const envelope = {
104
+ schemaVersion: COMMAND_ENVELOPE_SCHEMA_VERSION,
105
+ dryRun: false,
106
+ ...rest,
107
+ summary: { deleted: 0, created: 0, preserved: 0, ...summary },
108
+ };
109
+ const { valid, errors } = validateEnvelope(envelope);
110
+ if (!valid) {
111
+ throw new Error(`invalid command envelope: ${errors.join("; ")}`);
112
+ }
113
+ return Object.freeze(envelope);
114
+ }
115
+
116
+ /**
117
+ * Redact obvious secrets and personal data from a line bound for stderr.
118
+ *
119
+ * Best-effort by design: it is the last line of defence, not a substitute for
120
+ * not logging the value. Adapters must not put credentials in their narration.
121
+ * @param {string} text - Human-readable narration
122
+ * @returns {string} The same text with likely secrets and PII masked
123
+ */
124
+ export function redact(text) {
125
+ return String(text)
126
+ .replace(
127
+ /\b[\w.%+-]+@[\w-]+\.[A-Za-z]{2,}\b/gu,
128
+ match => `${match.slice(0, 2)}***@***`
129
+ )
130
+ .replace(
131
+ /((?:password|passwd|secret|token|api[-_]?key|authorization|bearer)["'\s:=]+)\S+/giu,
132
+ "$1***"
133
+ )
134
+ .replace(/\b(?:\d[ -]?){12,19}\b/gu, "***")
135
+ .replace(/\b\+?\d{10,15}\b/gu, "***");
136
+ }
137
+
138
+ /**
139
+ * Write the envelope to stdout as the process's single machine-readable result.
140
+ * @param {object} envelope - A built envelope
141
+ * @param {{ stdout?: { write: (chunk: string) => unknown } }} [io] - Injectable stream
142
+ * @returns {number} The exit code implied by the envelope's status
143
+ */
144
+ export function emitEnvelope(envelope, io = {}) {
145
+ const stdout = io.stdout ?? process.stdout;
146
+ stdout.write(`${JSON.stringify(envelope)}\n`);
147
+ return exitCodeForStatus(envelope.status);
148
+ }
149
+
150
+ /**
151
+ * CLI entry point.
152
+ * @param {string[]} argv - Arguments after the script name
153
+ * @returns {number} Process exit code
154
+ */
155
+ export function main(argv) {
156
+ if (argv.includes("--print-schema")) {
157
+ process.stdout.write(
158
+ `${JSON.stringify(COMMAND_ENVELOPE_SCHEMA, null, 2)}\n`
159
+ );
160
+ return 0;
161
+ }
162
+ const index = argv.indexOf("--validate");
163
+ if (index === -1) {
164
+ process.stderr.write(
165
+ "usage: lisa-command-envelope.mjs [--print-schema] [--validate <file|->]\n"
166
+ );
167
+ return 1;
168
+ }
169
+ const source = argv[index + 1] ?? "-";
170
+ let parsed;
171
+ try {
172
+ parsed = JSON.parse(fs.readFileSync(source === "-" ? 0 : source, "utf8"));
173
+ } catch (error) {
174
+ process.stderr.write(`[command-envelope] FAIL: ${redact(error.message)}\n`);
175
+ return 1;
176
+ }
177
+ const { valid, errors } = validateEnvelope(parsed);
178
+ if (!valid) {
179
+ process.stderr.write(
180
+ `[command-envelope] FAIL:\n${errors.map(entry => ` - ${entry}`).join("\n")}\n`
181
+ );
182
+ return 1;
183
+ }
184
+ process.stderr.write("[command-envelope] OK\n");
185
+ return 0;
186
+ }
187
+
188
+ // Run only when invoked directly — importing for tests must have no side effects.
189
+ if (
190
+ process.argv[1] &&
191
+ import.meta.url === pathToFileURL(process.argv[1]).href
192
+ ) {
193
+ process.exitCode = main(process.argv.slice(2));
194
+ }
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lisa-schema-validate — dependency-free validator for the JSON Schema subset
4
+ * Lisa's shipped contract schemas use.
5
+ *
6
+ * Lisa's copy-overwrite scripts run in every adopter repo before that repo has
7
+ * installed anything, so they cannot depend on a schema library. This module
8
+ * implements exactly the keywords the shipped schemas use and **rejects any
9
+ * keyword it does not implement**, so a schema can never silently validate less
10
+ * than it claims: an unsupported keyword is a validator error, not a pass.
11
+ *
12
+ * Supported: `type`, `const`, `enum`, `required`, `properties`,
13
+ * `additionalProperties` (boolean), `items`, `minLength`, `minimum`, `pattern`,
14
+ * `$ref` (local `#/$defs/<name>` only), `$defs`, plus the annotation-only
15
+ * keywords `$schema`, `$id`, `title`, `description`.
16
+ * @module scripts/lisa-schema-validate
17
+ */
18
+
19
+ /** Keywords that carry no validation semantics and are ignored. */
20
+ const ANNOTATION_KEYWORDS = new Set([
21
+ "$schema",
22
+ "$id",
23
+ "title",
24
+ "description",
25
+ "$defs",
26
+ "examples",
27
+ "default",
28
+ ]);
29
+
30
+ /** Keywords this validator implements. Anything else is an error. */
31
+ const SUPPORTED_KEYWORDS = new Set([
32
+ "type",
33
+ "const",
34
+ "enum",
35
+ "required",
36
+ "properties",
37
+ "additionalProperties",
38
+ "items",
39
+ "minLength",
40
+ "minimum",
41
+ "pattern",
42
+ "$ref",
43
+ ]);
44
+
45
+ /**
46
+ * JSON type name for a value, using JSON Schema's vocabulary.
47
+ * @param {unknown} value - Value to classify
48
+ * @returns {string} One of null/array/integer/number/string/boolean/object
49
+ */
50
+ export function jsonTypeOf(value) {
51
+ if (value === null) {
52
+ return "null";
53
+ }
54
+ if (Array.isArray(value)) {
55
+ return "array";
56
+ }
57
+ if (typeof value === "number") {
58
+ return Number.isInteger(value) ? "integer" : "number";
59
+ }
60
+ return typeof value;
61
+ }
62
+
63
+ /**
64
+ * Resolve a local `#/$defs/<name>` reference against the root schema.
65
+ * @param {string} ref - The `$ref` string
66
+ * @param {object} root - Root schema document
67
+ * @returns {object} The referenced subschema
68
+ * @throws {Error} When the reference is not a supported local $defs pointer
69
+ */
70
+ function resolveRef(ref, root) {
71
+ const match = /^#\/\$defs\/([^/]+)$/u.exec(ref);
72
+ const target = match ? root.$defs?.[match[1]] : undefined;
73
+ if (!target) {
74
+ throw new Error(
75
+ `unsupported or unresolvable $ref "${ref}" (only #/$defs/<name> is supported)`
76
+ );
77
+ }
78
+ return target;
79
+ }
80
+
81
+ /**
82
+ * Validate one value against one subschema, appending human-readable errors.
83
+ * @param {unknown} value - Value under validation
84
+ * @param {object} schema - Subschema to apply
85
+ * @param {object} root - Root schema document (for `$ref` resolution)
86
+ * @param {string} instancePath - JSON-pointer-ish path for messages
87
+ * @param {string[]} errors - Accumulator, mutated in place
88
+ * @returns {void}
89
+ */
90
+ function validateNode(value, schema, root, instancePath, errors) {
91
+ for (const keyword of Object.keys(schema)) {
92
+ if (!SUPPORTED_KEYWORDS.has(keyword) && !ANNOTATION_KEYWORDS.has(keyword)) {
93
+ throw new Error(
94
+ `schema at ${instancePath || "/"} uses unsupported keyword "${keyword}"`
95
+ );
96
+ }
97
+ }
98
+
99
+ if (typeof schema.$ref === "string") {
100
+ validateNode(
101
+ value,
102
+ resolveRef(schema.$ref, root),
103
+ root,
104
+ instancePath,
105
+ errors
106
+ );
107
+ return;
108
+ }
109
+
110
+ const where = instancePath || "(root)";
111
+ const actualType = jsonTypeOf(value);
112
+
113
+ if (typeof schema.type === "string") {
114
+ const ok =
115
+ schema.type === actualType ||
116
+ (schema.type === "number" && actualType === "integer");
117
+ if (!ok) {
118
+ errors.push(`${where}: expected type ${schema.type}, got ${actualType}`);
119
+ return;
120
+ }
121
+ }
122
+
123
+ if ("const" in schema && value !== schema.const) {
124
+ errors.push(
125
+ `${where}: expected constant ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`
126
+ );
127
+ }
128
+
129
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
130
+ errors.push(
131
+ `${where}: ${JSON.stringify(value)} is not one of ${schema.enum.map(entry => JSON.stringify(entry)).join(", ")}`
132
+ );
133
+ }
134
+
135
+ if (typeof value === "string") {
136
+ if (
137
+ typeof schema.minLength === "number" &&
138
+ value.length < schema.minLength
139
+ ) {
140
+ errors.push(`${where}: shorter than minLength ${schema.minLength}`);
141
+ }
142
+ if (
143
+ typeof schema.pattern === "string" &&
144
+ !new RegExp(schema.pattern, "u").test(value)
145
+ ) {
146
+ errors.push(`${where}: does not match pattern ${schema.pattern}`);
147
+ }
148
+ }
149
+
150
+ if (typeof value === "number" && typeof schema.minimum === "number") {
151
+ if (value < schema.minimum) {
152
+ errors.push(`${where}: below minimum ${schema.minimum}`);
153
+ }
154
+ }
155
+
156
+ if (actualType === "array" && schema.items) {
157
+ value.forEach((entry, index) => {
158
+ validateNode(entry, schema.items, root, `${where}[${index}]`, errors);
159
+ });
160
+ }
161
+
162
+ if (actualType !== "object") {
163
+ return;
164
+ }
165
+
166
+ for (const key of schema.required ?? []) {
167
+ if (!(key in value)) {
168
+ errors.push(`${where}: missing required property "${key}"`);
169
+ }
170
+ }
171
+
172
+ const properties = schema.properties ?? {};
173
+ for (const [key, subschema] of Object.entries(properties)) {
174
+ if (key in value) {
175
+ validateNode(
176
+ value[key],
177
+ subschema,
178
+ root,
179
+ `${instancePath}.${key}`,
180
+ errors
181
+ );
182
+ }
183
+ }
184
+
185
+ if (schema.additionalProperties === false) {
186
+ for (const key of Object.keys(value)) {
187
+ if (!(key in properties)) {
188
+ errors.push(`${where}: unexpected property "${key}"`);
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Validate a document against a schema.
196
+ * @param {unknown} document - The value to validate
197
+ * @param {object} schema - The schema document
198
+ * @returns {{ valid: boolean, errors: string[] }} Validation outcome
199
+ */
200
+ export function validateAgainstSchema(document, schema) {
201
+ const errors = [];
202
+ try {
203
+ validateNode(document, schema, schema, "", errors);
204
+ } catch (error) {
205
+ return { valid: false, errors: [`schema error: ${error.message}`] };
206
+ }
207
+ return { valid: errors.length === 0, errors };
208
+ }
@@ -0,0 +1,119 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://lisa.codyswann.dev/schemas/lisa-command-envelope.v1.schema.json",
4
+ "title": "Lisa standard command envelope (v1)",
5
+ "description": "The single JSON result every capability adapter (reset, seed, verify, inventory, classification) writes to stdout. Exit 0 means the operation completed AND verified; any other outcome exits nonzero and says which in `status`. Human-readable narration goes to stderr, redacted of secrets and personal data.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schemaVersion",
10
+ "capability",
11
+ "mode",
12
+ "operation",
13
+ "environment",
14
+ "contractVersion",
15
+ "dryRun",
16
+ "status",
17
+ "correlationId",
18
+ "summary"
19
+ ],
20
+ "properties": {
21
+ "schemaVersion": {
22
+ "const": "lisa-command-envelope-v1",
23
+ "description": "Pinned envelope schema version. A bump is a deliberate migration, never an incidental edit."
24
+ },
25
+ "capability": {
26
+ "type": "string",
27
+ "minLength": 1,
28
+ "pattern": "^[a-z][a-z0-9-]*$",
29
+ "description": "Which standard interface answered, e.g. reset, seed, reset-verify, state-inventory, state-classification."
30
+ },
31
+ "mode": {
32
+ "enum": ["real", "declared-noop", "not-applicable"],
33
+ "description": "real = the adapter does the thing. declared-noop = the repo deliberately has nothing to do here and says so machine-readably. not-applicable = the capability does not apply to this repo kind."
34
+ },
35
+ "operation": {
36
+ "type": "string",
37
+ "minLength": 1,
38
+ "description": "The concrete operation performed, in the adapter's own vocabulary."
39
+ },
40
+ "environment": {
41
+ "type": "string",
42
+ "minLength": 1,
43
+ "description": "The SERVER-RESOLVED environment identity the operation ran against. Never the caller-supplied --stage, which is only a request to be checked against this value."
44
+ },
45
+ "contractVersion": {
46
+ "type": "string",
47
+ "minLength": 1,
48
+ "description": "The project's own contract version for the state this adapter acts on, so evidence from one run cannot be read as evidence for a different contract."
49
+ },
50
+ "dryRun": {
51
+ "type": "boolean",
52
+ "description": "True when the adapter enumerated intended mutations without performing them. Mandatory support on every destructive adapter."
53
+ },
54
+ "status": {
55
+ "enum": [
56
+ "completed",
57
+ "no-op",
58
+ "not-adopted",
59
+ "detection-only",
60
+ "denied",
61
+ "invalid",
62
+ "failed",
63
+ "verification-mismatch"
64
+ ],
65
+ "description": "completed = did it and verified it. no-op = nothing to do (declared noop). not-adopted = the capability is not wired in this repo yet. detection-only = could not observe authoritative state, findings are non-authoritative. denied = refused by a guard. invalid = bad input or malformed contract. failed = attempted and failed. verification-mismatch = mutated but the post-state did not verify."
66
+ },
67
+ "correlationId": {
68
+ "type": "string",
69
+ "minLength": 1,
70
+ "description": "Stable id joining this envelope to the audit record and the CI log for the same run."
71
+ },
72
+ "summary": {
73
+ "type": "object",
74
+ "additionalProperties": true,
75
+ "required": ["deleted", "created", "preserved"],
76
+ "description": "Counts describing what the run did. Additional adapter-specific counters are allowed; these three are always present so a reader can compare runs across repos.",
77
+ "properties": {
78
+ "deleted": { "type": "integer", "minimum": 0 },
79
+ "created": { "type": "integer", "minimum": 0 },
80
+ "preserved": { "type": "integer", "minimum": 0 }
81
+ }
82
+ },
83
+ "reason": {
84
+ "type": "string",
85
+ "minLength": 1,
86
+ "description": "Required when mode is declared-noop or not-applicable, and whenever status is not a success status. Explains the outcome in one operator-readable sentence."
87
+ },
88
+ "owner": {
89
+ "type": "string",
90
+ "minLength": 1,
91
+ "description": "Required when mode is declared-noop or not-applicable. The team or role accountable for the declaration."
92
+ },
93
+ "capabilityManifest": {
94
+ "type": "string",
95
+ "minLength": 1,
96
+ "description": "Required when mode is declared-noop or not-applicable. Reference to the capability-matrix entry that records this declaration."
97
+ },
98
+ "idempotencyKey": {
99
+ "type": "string",
100
+ "minLength": 1,
101
+ "description": "Required in CI for destructive adapters so a retried invocation is recognized rather than repeated."
102
+ },
103
+ "findings": {
104
+ "type": "array",
105
+ "description": "Machine-readable findings. Each names the entity or subject, the rule violated, and a one-line message.",
106
+ "items": {
107
+ "type": "object",
108
+ "additionalProperties": true,
109
+ "required": ["code", "subject", "message"],
110
+ "properties": {
111
+ "code": { "type": "string", "minLength": 1 },
112
+ "subject": { "type": "string", "minLength": 1 },
113
+ "message": { "type": "string", "minLength": 1 },
114
+ "severity": { "enum": ["error", "warning"] }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }