@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.
- package/all/copy-overwrite/scripts/check-state-classification.mjs +609 -0
- package/all/copy-overwrite/scripts/lisa-command-envelope.mjs +194 -0
- package/all/copy-overwrite/scripts/lisa-schema-validate.mjs +208 -0
- package/all/copy-overwrite/scripts/schemas/lisa-command-envelope.v1.schema.json +119 -0
- package/all/copy-overwrite/scripts/schemas/lisa-state-contract.v1.schema.json +191 -0
- package/all/create-only/state/README.md +41 -0
- package/all/create-only/state/state-contract.example.json +46 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +92 -10
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/copy-overwrite/scripts/bdd/baseline.mjs +224 -0
- package/expo/copy-overwrite/scripts/bdd/contract.mjs +121 -0
- package/expo/copy-overwrite/scripts/bdd/envelope.mjs +231 -0
- package/expo/copy-overwrite/scripts/bdd/parse.mjs +220 -0
- package/expo/copy-overwrite/scripts/bdd/render.mjs +170 -0
- package/expo/copy-overwrite/scripts/bdd/report.mjs +361 -0
- package/expo/copy-overwrite/scripts/bdd/validate.mjs +282 -0
- package/expo/copy-overwrite/scripts/bdd/waivers.mjs +210 -0
- package/expo/copy-overwrite/scripts/bdd-matrix.mjs +278 -0
- package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +657 -0
- package/expo/create-only/bdd/coverage-map.json +38 -0
- package/expo/create-only/bdd/features/.keep +5 -0
- package/expo/github-rulesets/bdd-coverage.json +33 -0
- package/expo/package-lisa/package.lisa.json +3 -0
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-acceptance-criteria/SKILL.md +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-codify-verification/SKILL.md +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-research/SKILL.md +7 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-task-decomposition/SKILL.md +2 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-test-strategy/SKILL.md +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-verification-lifecycle/SKILL.md +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-verify/SKILL.md +1 -1
- package/plugins/lisa/rules/eager/reset-seed-coverage.md +48 -0
- package/plugins/lisa/rules/reference/reset-seed-coverage.md +231 -0
- package/plugins/lisa/skills/lisa-acceptance-criteria/SKILL.md +1 -0
- package/plugins/lisa/skills/lisa-codify-verification/SKILL.md +4 -0
- package/plugins/lisa/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa/skills/lisa-research/SKILL.md +7 -1
- package/plugins/lisa/skills/lisa-task-decomposition/SKILL.md +2 -0
- package/plugins/lisa/skills/lisa-test-strategy/SKILL.md +1 -0
- package/plugins/lisa/skills/lisa-verification-lifecycle/SKILL.md +1 -0
- package/plugins/lisa/skills/lisa-verify/SKILL.md +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-acceptance-criteria/SKILL.md +1 -0
- package/plugins/lisa-agy/skills/lisa-codify-verification/SKILL.md +4 -0
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa-agy/skills/lisa-research/SKILL.md +7 -1
- package/plugins/lisa-agy/skills/lisa-task-decomposition/SKILL.md +2 -0
- package/plugins/lisa-agy/skills/lisa-test-strategy/SKILL.md +1 -0
- package/plugins/lisa-agy/skills/lisa-verification-lifecycle/SKILL.md +1 -0
- package/plugins/lisa-agy/skills/lisa-verify/SKILL.md +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/rules/eager/reset-seed-coverage.md +48 -0
- package/plugins/lisa-copilot/rules/reference/reset-seed-coverage.md +231 -0
- package/plugins/lisa-copilot/skills/lisa-acceptance-criteria/SKILL.md +1 -0
- package/plugins/lisa-copilot/skills/lisa-codify-verification/SKILL.md +4 -0
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa-copilot/skills/lisa-research/SKILL.md +7 -1
- package/plugins/lisa-copilot/skills/lisa-task-decomposition/SKILL.md +2 -0
- package/plugins/lisa-copilot/skills/lisa-test-strategy/SKILL.md +1 -0
- package/plugins/lisa-copilot/skills/lisa-verification-lifecycle/SKILL.md +1 -0
- package/plugins/lisa-copilot/skills/lisa-verify/SKILL.md +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/reset-seed-coverage-reference.mdc +236 -0
- package/plugins/lisa-cursor/rules/reset-seed-coverage.mdc +53 -0
- package/plugins/lisa-cursor/skills/lisa-acceptance-criteria/SKILL.md +1 -0
- package/plugins/lisa-cursor/skills/lisa-codify-verification/SKILL.md +4 -0
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/lisa-cursor/skills/lisa-research/SKILL.md +7 -1
- package/plugins/lisa-cursor/skills/lisa-task-decomposition/SKILL.md +2 -0
- package/plugins/lisa-cursor/skills/lisa-test-strategy/SKILL.md +1 -0
- package/plugins/lisa-cursor/skills/lisa-verification-lifecycle/SKILL.md +1 -0
- package/plugins/lisa-cursor/skills/lisa-verify/SKILL.md +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/skills/ops-db-ops/SKILL.md +93 -2
- package/plugins/lisa-expo/skills/ops-db-ops/SKILL.md +94 -3
- package/plugins/lisa-expo/skills/ops-db-ops/agents/openai.yaml +2 -2
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-agy/skills/ops-db-ops/SKILL.md +94 -3
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/skills/ops-db-ops/SKILL.md +94 -3
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/skills/ops-db-ops/SKILL.md +94 -3
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/rules/eager/reset-seed-coverage.md +48 -0
- package/plugins/src/base/rules/reference/reset-seed-coverage.md +231 -0
- package/plugins/src/base/skills/lisa-acceptance-criteria/SKILL.md +1 -0
- package/plugins/src/base/skills/lisa-codify-verification/SKILL.md +4 -0
- package/plugins/src/base/skills/lisa-implement/SKILL.md +2 -0
- package/plugins/src/base/skills/lisa-research/SKILL.md +7 -1
- package/plugins/src/base/skills/lisa-task-decomposition/SKILL.md +2 -0
- package/plugins/src/base/skills/lisa-test-strategy/SKILL.md +1 -0
- package/plugins/src/base/skills/lisa-verification-lifecycle/SKILL.md +1 -0
- package/plugins/src/base/skills/lisa-verify/SKILL.md +1 -1
- package/plugins/src/expo/skills/ops-db-ops/SKILL.md +94 -3
- 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
|
+
}
|