@stacksjs/buddy 0.70.163 → 0.70.164
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/README.md +1 -1
- package/dist/cli.js +1 -0
- package/dist/commands/ai-context.js +1 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/deploy.d.ts +11 -1
- package/dist/commands/deploy.js +22 -4
- package/dist/commands/dev.js +44 -14
- package/dist/commands/docs/buddy-commands.d.ts +8 -0
- package/dist/commands/docs/buddy-commands.js +116 -0
- package/dist/commands/docs/buddy-commands.test.d.ts +1 -0
- package/dist/commands/docs/buddy-commands.test.js +38 -0
- package/dist/commands/docs/generated-artifacts.d.ts +3 -0
- package/dist/commands/docs/generated-artifacts.js +69 -0
- package/dist/commands/docs/generated-artifacts.test.d.ts +1 -0
- package/dist/commands/docs/generated-artifacts.test.js +19 -0
- package/dist/commands/docs/links.d.ts +18 -0
- package/dist/commands/docs/links.js +83 -0
- package/dist/commands/docs/links.test.d.ts +1 -0
- package/dist/commands/docs/links.test.js +73 -0
- package/dist/commands/docs.d.ts +7 -0
- package/dist/commands/docs.js +24 -0
- package/dist/commands/doctor.js +8 -0
- package/dist/commands/env.d.ts +14 -0
- package/dist/commands/env.js +73 -7
- package/dist/commands/generate.js +1 -1
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/mail.js +1 -1
- package/dist/commands/migrate.js +4 -4
- package/dist/commands/protocol/craft-evidence.d.ts +76 -0
- package/dist/commands/protocol/craft-evidence.js +98 -0
- package/dist/commands/protocol/craft-evidence.test.d.ts +1 -0
- package/dist/commands/protocol/craft-evidence.test.js +20 -0
- package/dist/commands/protocol/desktop-lifecycle-report.d.ts +16 -0
- package/dist/commands/protocol/desktop-lifecycle-report.js +57 -0
- package/dist/commands/protocol/desktop-lifecycle-report.test.d.ts +1 -0
- package/dist/commands/protocol/desktop-lifecycle-report.test.js +32 -0
- package/dist/commands/protocol/desktop-support.d.ts +1 -0
- package/dist/commands/protocol/desktop-support.js +32 -0
- package/dist/commands/protocol/desktop-workflow.test.d.ts +1 -0
- package/dist/commands/protocol/desktop-workflow.test.js +33 -0
- package/dist/commands/protocol/driver-registry.d.ts +1 -0
- package/dist/commands/protocol/driver-registry.js +71 -0
- package/dist/commands/protocol/pantry-evidence.d.ts +67 -0
- package/dist/commands/protocol/pantry-evidence.js +144 -0
- package/dist/commands/protocol/pantry-evidence.test.d.ts +1 -0
- package/dist/commands/protocol/pantry-evidence.test.js +29 -0
- package/dist/commands/protocol/release-manifest.d.ts +30 -0
- package/dist/commands/protocol/release-manifest.js +102 -0
- package/dist/commands/protocol/release-manifest.test.d.ts +1 -0
- package/dist/commands/protocol/release-manifest.test.js +34 -0
- package/dist/commands/protocol/run-conformance.d.ts +21 -0
- package/dist/commands/protocol/run-conformance.js +358 -0
- package/dist/commands/protocol/run-conformance.test.d.ts +1 -0
- package/dist/commands/protocol/run-conformance.test.js +52 -0
- package/dist/commands/protocol/run-driver-contracts.d.ts +7 -0
- package/dist/commands/protocol/run-driver-contracts.js +94 -0
- package/dist/commands/protocol/run-driver-contracts.test.d.ts +1 -0
- package/dist/commands/protocol/run-driver-contracts.test.js +18 -0
- package/dist/commands/protocol/source-manifest.d.ts +25 -0
- package/dist/commands/protocol/source-manifest.js +113 -0
- package/dist/commands/protocol/sync-suite.d.ts +8 -0
- package/dist/commands/protocol/sync-suite.js +137 -0
- package/dist/commands/protocol/sync-suite.test.d.ts +1 -0
- package/dist/commands/protocol/sync-suite.test.js +44 -0
- package/dist/commands/protocol.d.ts +8 -0
- package/dist/commands/protocol.js +60 -0
- package/dist/commands/publish.js +149 -0
- package/dist/commands/run-tool.d.ts +7 -0
- package/dist/commands/run-tool.js +9 -0
- package/dist/commands/seed.js +15 -40
- package/dist/commands/serve.d.ts +18 -5
- package/dist/commands/serve.js +21 -2
- package/dist/commands/setup-ai.d.ts +39 -0
- package/dist/commands/setup-ai.js +121 -0
- package/dist/commands/setup.js +22 -0
- package/dist/lazy-commands.js +27 -1
- package/package.json +43 -42
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { arch, platform } from "node:os";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
6
|
+
import addFormats from "ajv-formats";
|
|
7
|
+
import { Action } from "../../../../actions/src/index";
|
|
8
|
+
import { assertCapabilityAvailable, capabilityRegistry } from "../../../../config/src/capabilities";
|
|
9
|
+
import { decryptValue, encryptValue, generateKeypair } from "../../../../env/src/crypto";
|
|
10
|
+
import { escapeHtml } from "../../../../error-handling/src/error-page-template";
|
|
11
|
+
import { appPath, defaultsAppPath } from "../../../../path/src/index";
|
|
12
|
+
import { createValidationErrorResponse } from "../../../../router/src/error-handler";
|
|
13
|
+
import { timingSafeEqualString } from "../../../../security/src/hash";
|
|
14
|
+
import { object, string } from "../../../../validation/src/index";
|
|
15
|
+
const root = resolve(import.meta.dir, "../../../../../../.."), suiteRoot = resolve(root, ".github/protocol/suite/1.0-draft");
|
|
16
|
+
function git(...arguments_) {
|
|
17
|
+
const result = Bun.spawnSync(["git", ...arguments_], { cwd: root });
|
|
18
|
+
if (result.exitCode !== 0)
|
|
19
|
+
throw Error(result.stderr.toString().trim());
|
|
20
|
+
return result.stdout.toString().trim();
|
|
21
|
+
}
|
|
22
|
+
function exactSourceDigest() {
|
|
23
|
+
const tree = git("rev-parse", "HEAD^{tree}");
|
|
24
|
+
return `sha256:${createHash("sha256").update(tree).digest("hex")}`;
|
|
25
|
+
}
|
|
26
|
+
function tamper(ciphertext) {
|
|
27
|
+
const prefix = "encrypted:", payload = Buffer.from(ciphertext.slice(prefix.length), "base64"), last = payload.length - 1;
|
|
28
|
+
if (last >= 0)
|
|
29
|
+
payload[last] = (payload[last] ?? 0) ^ 1;
|
|
30
|
+
return `${prefix}${payload.toString("base64")}`;
|
|
31
|
+
}
|
|
32
|
+
function blob(revision, path) {
|
|
33
|
+
return `https://github.com/stacksjs/stacks/blob/${revision}/${path}`;
|
|
34
|
+
}
|
|
35
|
+
export function executeSecurityEvidence(revision) {
|
|
36
|
+
const evidence = new Map, cryptoUrl = blob(revision, "storage/framework/core/env/src/crypto.ts"), comparisonUrl = blob(revision, "storage/framework/core/security/src/hash.ts"), renderUrl = blob(revision, "storage/framework/core/error-handling/src/error-page-template.ts"), started = performance.now();
|
|
37
|
+
try {
|
|
38
|
+
const { publicKey, privateKey } = generateKeypair(), encrypted = encryptValue("secret", publicKey), roundTrip = decryptValue(encrypted, privateKey);
|
|
39
|
+
let tamperRejected = !1;
|
|
40
|
+
try {
|
|
41
|
+
decryptValue(tamper(encrypted), privateKey);
|
|
42
|
+
} catch {
|
|
43
|
+
tamperRejected = !0;
|
|
44
|
+
}
|
|
45
|
+
const passed = roundTrip === "secret" && !encrypted.includes("secret") && tamperRejected;
|
|
46
|
+
evidence.set("CORE-SEC-05", {
|
|
47
|
+
status: passed ? "pass" : "fail",
|
|
48
|
+
evidenceUrl: cryptoUrl,
|
|
49
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
50
|
+
notes: passed ? "Authenticated-encryption round trip passed and modified ciphertext was rejected." : "Authenticated-encryption fixture failed."
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
evidence.set("CORE-SEC-05", {
|
|
54
|
+
status: "fail",
|
|
55
|
+
evidenceUrl: cryptoUrl,
|
|
56
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
57
|
+
notes: `Authenticated-encryption fixture threw: ${error instanceof Error ? error.message : String(error)}`
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const comparisonStarted = performance.now(), unequal = timingSafeEqualString("token-a", "token-b") === !1;
|
|
61
|
+
evidence.set("CORE-SEC-07", {
|
|
62
|
+
status: unequal ? "pass" : "fail",
|
|
63
|
+
evidenceUrl: comparisonUrl,
|
|
64
|
+
durationMs: Math.max(0, performance.now() - comparisonStarted),
|
|
65
|
+
notes: unequal ? "The public authenticator comparison helper uses the runtime timing-safe primitive." : "Timing-safe comparison fixture failed."
|
|
66
|
+
});
|
|
67
|
+
const renderStarted = performance.now(), renderOk = escapeHtml("<script>alert(1)</script>") === "<script>alert(1)</script>";
|
|
68
|
+
evidence.set("CORE-SEC-02", {
|
|
69
|
+
status: renderOk ? "pass" : "fail",
|
|
70
|
+
evidenceUrl: renderUrl,
|
|
71
|
+
durationMs: Math.max(0, performance.now() - renderStarted),
|
|
72
|
+
notes: renderOk ? "Untrusted markup is converted to HTML entities by the default escaper." : "Default render-escaping fixture failed."
|
|
73
|
+
});
|
|
74
|
+
return evidence;
|
|
75
|
+
}
|
|
76
|
+
export function executeConfigEvidence(revision) {
|
|
77
|
+
const evidence = new Map, url = blob(revision, "storage/framework/core/config/src/capabilities.ts"), started = performance.now();
|
|
78
|
+
let failedClosed = !1, message = "";
|
|
79
|
+
try {
|
|
80
|
+
assertCapabilityAvailable("queue", "planned");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
failedClosed = !0;
|
|
83
|
+
message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
}
|
|
85
|
+
evidence.set("CORE-CONFIG-02", {
|
|
86
|
+
status: failedClosed ? "pass" : "fail",
|
|
87
|
+
evidenceUrl: url,
|
|
88
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
89
|
+
notes: failedClosed ? `Selecting an unavailable driver is rejected before use: ${message}` : "Fail-closed capability guard accepted an unavailable driver."
|
|
90
|
+
});
|
|
91
|
+
const drvStarted = performance.now();
|
|
92
|
+
let drvFailedClosed = !1;
|
|
93
|
+
try {
|
|
94
|
+
assertCapabilityAvailable("queue", "planned-driver");
|
|
95
|
+
} catch {
|
|
96
|
+
drvFailedClosed = !0;
|
|
97
|
+
}
|
|
98
|
+
evidence.set("STD-DRV-01", {
|
|
99
|
+
status: drvFailedClosed ? "pass" : "fail",
|
|
100
|
+
evidenceUrl: url,
|
|
101
|
+
durationMs: Math.max(0, performance.now() - drvStarted),
|
|
102
|
+
notes: drvFailedClosed ? "A planned driver is rejected loudly with no silent fallback." : "Driver-failure fixture did not reject a planned driver."
|
|
103
|
+
});
|
|
104
|
+
return evidence;
|
|
105
|
+
}
|
|
106
|
+
export async function executeValidationEvidence(revision) {
|
|
107
|
+
const evidence = new Map, validationUrl = blob(revision, "storage/framework/core/validation/src/validator.ts"), envelopeUrl = blob(revision, "storage/framework/core/router/src/error-handler.ts"), started = performance.now(), result = await object({ profile: object({ email: string().email() }) }).validate({ profile: { email: "bad" } }), valOk = result.valid === !1 && Object.keys(result.errors).includes("profile.email");
|
|
108
|
+
evidence.set("CORE-VAL-01", {
|
|
109
|
+
status: valOk ? "pass" : "fail",
|
|
110
|
+
evidenceUrl: validationUrl,
|
|
111
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
112
|
+
notes: valOk ? "Nested invalid input produced a stable field-keyed error at profile.email." : "Field-keyed validation fixture failed."
|
|
113
|
+
});
|
|
114
|
+
const envStarted = performance.now(), fieldErrors = {};
|
|
115
|
+
for (const [field, errors] of Object.entries(result.errors))
|
|
116
|
+
fieldErrors[field] = errors.map((entry) => entry.message ?? String(entry));
|
|
117
|
+
const response = createValidationErrorResponse(fieldErrors, new Request("http://conformance.local/profile", { method: "POST" })), body = await response.text(), durationEnv = Math.max(0, performance.now() - envStarted), envelopeOk = response.status === 422 && body.includes('"message":"Validation failed"') && body.includes("profile.email");
|
|
118
|
+
evidence.set("CORE-ERR-01", {
|
|
119
|
+
status: envelopeOk ? "pass" : "fail",
|
|
120
|
+
evidenceUrl: envelopeUrl,
|
|
121
|
+
durationMs: durationEnv,
|
|
122
|
+
notes: envelopeOk ? "Validation failure serialized to a stable 422 envelope with error, message, status, and field errors." : "Error-envelope fixture failed."
|
|
123
|
+
});
|
|
124
|
+
const leaks = ["stack", "password", "SELECT", "/home/"].filter((token) => body.includes(token));
|
|
125
|
+
evidence.set("CORE-ERR-02", {
|
|
126
|
+
status: envelopeOk && leaks.length === 0 ? "pass" : "fail",
|
|
127
|
+
evidenceUrl: envelopeUrl,
|
|
128
|
+
durationMs: durationEnv,
|
|
129
|
+
notes: leaks.length === 0 ? "Production error envelope contained no stack trace, credentials, raw query, or filesystem path." : `Production error envelope leaked: ${leaks.join(", ")}.`
|
|
130
|
+
});
|
|
131
|
+
return evidence;
|
|
132
|
+
}
|
|
133
|
+
export function executeConventionsEvidence(revision) {
|
|
134
|
+
const evidence = new Map, rolesUrl = blob(revision, "storage/framework/core/path/src/index.ts"), overrideUrl = blob(revision, "storage/framework/core/router/src/stacks-router.ts"), started = performance.now(), conv01Ok = appPath("Actions/Greet").endsWith("/app/Actions/Greet");
|
|
135
|
+
evidence.set("CORE-CONV-01", {
|
|
136
|
+
status: conv01Ok ? "pass" : "fail",
|
|
137
|
+
evidenceUrl: rolesUrl,
|
|
138
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
139
|
+
notes: conv01Ok ? "The Action role maps to its documented app/Actions/<name> path." : "Role-to-path mapping fixture failed."
|
|
140
|
+
});
|
|
141
|
+
const overrideStarted = performance.now(), logical = "Actions/NotifyUser.ts", userCandidate = appPath(logical), defaultCandidate = defaultsAppPath(logical), resolved = existsSync(userCandidate) ? userCandidate : defaultCandidate, conv02Ok = existsSync(userCandidate) && existsSync(defaultCandidate) && resolved === userCandidate;
|
|
142
|
+
evidence.set("CORE-CONV-02", {
|
|
143
|
+
status: conv02Ok ? "pass" : "fail",
|
|
144
|
+
evidenceUrl: overrideUrl,
|
|
145
|
+
durationMs: Math.max(0, performance.now() - overrideStarted),
|
|
146
|
+
notes: conv02Ok ? "With the same logical path present in both app/ and framework defaults, resolution selects the app-owned file." : "Override-precedence fixture failed."
|
|
147
|
+
});
|
|
148
|
+
return evidence;
|
|
149
|
+
}
|
|
150
|
+
export async function executeLifecycleEvidence(revision) {
|
|
151
|
+
const evidence = new Map, url = blob(revision, "storage/framework/core/actions/src/action.ts"), started = performance.now(), mvaOk = (await new Action({
|
|
152
|
+
name: "Increment",
|
|
153
|
+
handle(request) {
|
|
154
|
+
return { value: request.body.value + 1 };
|
|
155
|
+
}
|
|
156
|
+
}).handle({ body: { value: 1 } }))?.value === 2;
|
|
157
|
+
evidence.set("CORE-MVA-01", {
|
|
158
|
+
status: mvaOk ? "pass" : "fail",
|
|
159
|
+
evidenceUrl: url,
|
|
160
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
161
|
+
notes: mvaOk ? "An Action was invoked directly with no HTTP transport and returned its computed result." : "Direct Action invocation fixture failed."
|
|
162
|
+
});
|
|
163
|
+
return evidence;
|
|
164
|
+
}
|
|
165
|
+
export async function executeQueryEvidence(revision) {
|
|
166
|
+
const evidence = new Map, url = blob(revision, "storage/framework/core/database/src/types.ts"), started = performance.now();
|
|
167
|
+
try {
|
|
168
|
+
const { sql } = await import("../../../../database/src/types"), attack = "' OR 1=1 --", query = sql`SELECT * FROM users WHERE email = ${attack}`, parameterized = query.sql === "SELECT * FROM users WHERE email = ?" && Array.isArray(query.parameters) && query.parameters.length === 1 && query.parameters[0] === attack;
|
|
169
|
+
evidence.set("CORE-SEC-01", {
|
|
170
|
+
status: parameterized ? "pass" : "fail",
|
|
171
|
+
evidenceUrl: url,
|
|
172
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
173
|
+
notes: parameterized ? "Untrusted input is bound as a positional parameter and never inlined into the SQL text." : "Query-parameterization fixture failed."
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
evidence.set("CORE-SEC-01", {
|
|
177
|
+
status: "skipped",
|
|
178
|
+
evidenceUrl: null,
|
|
179
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
180
|
+
notes: `Query builder unavailable in this environment: ${error instanceof Error ? error.message.slice(0, 100) : String(error)}`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return evidence;
|
|
184
|
+
}
|
|
185
|
+
export async function executeDatabaseEvidence(revision) {
|
|
186
|
+
const evidence = new Map, url = blob(revision, "storage/framework/core/query-builder/src/index.ts"), started = performance.now();
|
|
187
|
+
try {
|
|
188
|
+
const { createQueryBuilder, setConfig } = await import("../../../../query-builder/src/index");
|
|
189
|
+
setConfig({ dialect: "sqlite", database: ":memory:" });
|
|
190
|
+
const db = createQueryBuilder();
|
|
191
|
+
await db.raw`DROP TABLE IF EXISTS conformance_widgets`.execute();
|
|
192
|
+
await db.raw`CREATE TABLE conformance_widgets (id INTEGER PRIMARY KEY, name TEXT)`.execute();
|
|
193
|
+
await db.insertInto("conformance_widgets").values({ id: 1, name: "a" }).execute();
|
|
194
|
+
const created = await db.selectFrom("conformance_widgets").selectAll().execute();
|
|
195
|
+
await db.updateTable("conformance_widgets").set({ name: "b" }).where("id", "=", 1).execute();
|
|
196
|
+
const updated = await db.selectFrom("conformance_widgets").select(["name"]).where("id", "=", 1).execute();
|
|
197
|
+
await db.deleteFrom("conformance_widgets").where("id", "=", 1).execute();
|
|
198
|
+
const afterDelete = await db.selectFrom("conformance_widgets").selectAll().execute(), crudOk = created.length === 1 && created[0]?.name === "a" && updated[0]?.name === "b" && afterDelete.length === 0;
|
|
199
|
+
evidence.set("CORE-DATA-01", {
|
|
200
|
+
status: crudOk ? "pass" : "fail",
|
|
201
|
+
evidenceUrl: url,
|
|
202
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
203
|
+
notes: crudOk ? "Create, read, update, and delete round-trip succeeded against SQLite." : "CRUD fixture failed."
|
|
204
|
+
});
|
|
205
|
+
const txStarted = performance.now();
|
|
206
|
+
await db.insertInto("conformance_widgets").values({ id: 1, name: "keep" }).execute();
|
|
207
|
+
let rolledBack = !1;
|
|
208
|
+
try {
|
|
209
|
+
await db.transaction(async (trx) => {
|
|
210
|
+
await trx.insertInto("conformance_widgets").values({ id: 2, name: "temp" }).execute();
|
|
211
|
+
throw Error("force rollback");
|
|
212
|
+
});
|
|
213
|
+
} catch {
|
|
214
|
+
rolledBack = !0;
|
|
215
|
+
}
|
|
216
|
+
const rows = await db.selectFrom("conformance_widgets").selectAll().execute(), txOk = rolledBack && rows.length === 1 && rows[0]?.id === 1;
|
|
217
|
+
evidence.set("CORE-DATA-03", {
|
|
218
|
+
status: txOk ? "pass" : "fail",
|
|
219
|
+
evidenceUrl: url,
|
|
220
|
+
durationMs: Math.max(0, performance.now() - txStarted),
|
|
221
|
+
notes: txOk ? "A failing transaction rolled back its writes, leaving prior state intact." : "Transaction-rollback fixture failed."
|
|
222
|
+
});
|
|
223
|
+
const relStarted = performance.now();
|
|
224
|
+
await db.raw`DROP TABLE IF EXISTS conformance_books`.execute();
|
|
225
|
+
await db.raw`DROP TABLE IF EXISTS conformance_authors`.execute();
|
|
226
|
+
await db.raw`CREATE TABLE conformance_authors (id INTEGER PRIMARY KEY, name TEXT)`.execute();
|
|
227
|
+
await db.raw`CREATE TABLE conformance_books (id INTEGER PRIMARY KEY, author_id INTEGER, title TEXT)`.execute();
|
|
228
|
+
await db.insertInto("conformance_authors").values({ id: 1, name: "Ada" }).execute();
|
|
229
|
+
await db.insertInto("conformance_books").values([{ id: 1, author_id: 1, title: "B1" }, { id: 2, author_id: 1, title: "B2" }]).execute();
|
|
230
|
+
const related = await db.selectFrom("conformance_books").innerJoin("conformance_authors", "conformance_authors.id", "=", "conformance_books.author_id").select(["conformance_books.title", "conformance_authors.name"]).where("conformance_authors.id", "=", 1).execute(), relationOk = related.length === 2 && related.every((row) => row.name === "Ada");
|
|
231
|
+
evidence.set("CORE-DATA-02", {
|
|
232
|
+
status: relationOk ? "pass" : "fail",
|
|
233
|
+
evidenceUrl: url,
|
|
234
|
+
durationMs: Math.max(0, performance.now() - relStarted),
|
|
235
|
+
notes: relationOk ? "A relation query joined related rows across two tables." : "Relation fixture failed."
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const note = `Query builder unavailable in this environment: ${error instanceof Error ? error.message.slice(0, 100) : String(error)}`;
|
|
239
|
+
for (const id of ["CORE-DATA-01", "CORE-DATA-02", "CORE-DATA-03"])
|
|
240
|
+
if (!evidence.has(id))
|
|
241
|
+
evidence.set(id, { status: "skipped", evidenceUrl: null, durationMs: Math.max(0, performance.now() - started), notes: note });
|
|
242
|
+
}
|
|
243
|
+
return evidence;
|
|
244
|
+
}
|
|
245
|
+
function validateSemantics(report, catalog, fixtures) {
|
|
246
|
+
const errors = [], expected = new Set(catalog.requirements.map((requirement) => requirement.id)), seen = new Set, fixtureIds = new Set(fixtures.fixtures.map((fixture) => fixture.id));
|
|
247
|
+
for (const result of report.results) {
|
|
248
|
+
if (seen.has(result.requirementId))
|
|
249
|
+
errors.push(`${result.requirementId}: duplicate result`);
|
|
250
|
+
if (!expected.has(result.requirementId))
|
|
251
|
+
errors.push(`${result.requirementId}: unknown requirement`);
|
|
252
|
+
if (result.fixtureId && !fixtureIds.has(result.fixtureId))
|
|
253
|
+
errors.push(`${result.requirementId}: unknown fixture`);
|
|
254
|
+
if (result.status === "pass" && !result.evidenceUrl)
|
|
255
|
+
errors.push(`${result.requirementId}: pass requires evidence`);
|
|
256
|
+
seen.add(result.requirementId);
|
|
257
|
+
}
|
|
258
|
+
for (const id of expected)
|
|
259
|
+
if (!seen.has(id))
|
|
260
|
+
errors.push(`${id}: missing result`);
|
|
261
|
+
if (report.profileClaim !== null)
|
|
262
|
+
errors.push("The Stacks adapter must remain unverified until profile calculation is implemented");
|
|
263
|
+
return errors;
|
|
264
|
+
}
|
|
265
|
+
function renderSummary(report) {
|
|
266
|
+
const counts = new Map;
|
|
267
|
+
for (const result of report.results)
|
|
268
|
+
counts.set(result.status, (counts.get(result.status) || 0) + 1);
|
|
269
|
+
const rows = [...counts].sort(([a], [b]) => a.localeCompare(b)).map(([status, count]) => `| ${status} | ${count} |`).join(`
|
|
270
|
+
`);
|
|
271
|
+
return `# Stacks protocol conformance report
|
|
272
|
+
|
|
273
|
+
**Profile claim:** Unverified
|
|
274
|
+
**Implementation:** \`${report.implementation.revision}\` (${report.implementation.version})
|
|
275
|
+
**Protocol:** ${report.protocol.version}, catalog ${report.protocol.catalogRevision}, suite ${report.protocol.suiteVersion}
|
|
276
|
+
**Runtime/platform:** Bun ${report.execution.runtime.version} on ${report.execution.platform.os}/${report.execution.platform.architecture}
|
|
277
|
+
**CI:** [run](${report.execution.ci.runUrl}) \xB7 [artifact](${report.execution.ci.artifactUrl})
|
|
278
|
+
|
|
279
|
+
| Status | Count |
|
|
280
|
+
| --- | ---: |
|
|
281
|
+
${rows}
|
|
282
|
+
|
|
283
|
+
Only executable adapter checks are marked \`pass\`. All remaining requirements stay \`skipped\` until their fixtures are wired to Stacks public APIs.
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
export async function buildReport() {
|
|
287
|
+
const startedAt = new Date, catalog = JSON.parse(readFileSync(resolve(suiteRoot, "catalog.json"), "utf8")), fixtures = JSON.parse(readFileSync(resolve(suiteRoot, "fixtures/conformance.json"), "utf8")), lock = JSON.parse(readFileSync(resolve(root, ".github/protocol/suite.lock.json"), "utf8")), packageJson = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8")), revision = process.env.GITHUB_SHA || git("rev-parse", "HEAD"), runUrl = process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` : `https://github.com/stacksjs/stacks/commit/${revision}`, evidence = executeSecurityEvidence(revision);
|
|
288
|
+
for (const [id, value] of executeConfigEvidence(revision))
|
|
289
|
+
evidence.set(id, value);
|
|
290
|
+
for (const [id, value] of executeConventionsEvidence(revision))
|
|
291
|
+
evidence.set(id, value);
|
|
292
|
+
for (const [id, value] of await executeValidationEvidence(revision))
|
|
293
|
+
evidence.set(id, value);
|
|
294
|
+
for (const [id, value] of await executeLifecycleEvidence(revision))
|
|
295
|
+
evidence.set(id, value);
|
|
296
|
+
for (const [id, value] of await executeQueryEvidence(revision))
|
|
297
|
+
evidence.set(id, value);
|
|
298
|
+
for (const [id, value] of await executeDatabaseEvidence(revision))
|
|
299
|
+
evidence.set(id, value);
|
|
300
|
+
const fixtureByRequirement = new Map(fixtures.fixtures.flatMap((fixture) => fixture.requirements.map((id) => [id, fixture.id]))), results = catalog.requirements.map((requirement) => ({
|
|
301
|
+
requirementId: requirement.id,
|
|
302
|
+
fixtureId: fixtureByRequirement.get(requirement.id) || null,
|
|
303
|
+
...evidence.get(requirement.id) || {
|
|
304
|
+
status: "skipped",
|
|
305
|
+
evidenceUrl: null,
|
|
306
|
+
durationMs: 0,
|
|
307
|
+
notes: requirement.evidence === "behavior" ? "Fixture adapter is not implemented yet." : "Inspection evidence is not linked yet."
|
|
308
|
+
}
|
|
309
|
+
})), drivers = capabilityRegistry.map((driver) => ({
|
|
310
|
+
category: driver.category,
|
|
311
|
+
name: driver.name,
|
|
312
|
+
version: packageJson.version,
|
|
313
|
+
serviceVersion: null,
|
|
314
|
+
topology: driver.topology,
|
|
315
|
+
status: driver.status,
|
|
316
|
+
evidenceUrl: driver.testEvidence[0] ? `https://github.com/stacksjs/stacks/blob/${revision}/${driver.testEvidence[0]}` : null,
|
|
317
|
+
prerequisites: driver.prerequisites
|
|
318
|
+
}));
|
|
319
|
+
return {
|
|
320
|
+
reportVersion: "1.0.0-draft.1",
|
|
321
|
+
protocol: { version: catalog.protocolVersion, catalogRevision: catalog.catalogRevision, suiteVersion: fixtures.suiteVersion, rfcsRevision: lock.rfcsRevision },
|
|
322
|
+
implementation: { name: "Stacks", version: packageJson.version, revision, repository: "https://github.com/stacksjs/stacks", sourceDigest: exactSourceDigest() },
|
|
323
|
+
execution: {
|
|
324
|
+
startedAt: startedAt.toISOString(),
|
|
325
|
+
completedAt: new Date().toISOString(),
|
|
326
|
+
runtime: { name: "bun", version: Bun.version },
|
|
327
|
+
platform: { os: platform(), architecture: arch() },
|
|
328
|
+
ci: { provider: process.env.GITHUB_ACTIONS ? "github-actions" : "local", runUrl, artifactUrl: `${runUrl}#artifacts` }
|
|
329
|
+
},
|
|
330
|
+
profileClaim: null,
|
|
331
|
+
results,
|
|
332
|
+
drivers,
|
|
333
|
+
extensions: catalog.extensions.map((extension) => extension.id === "EXT-DESKTOP-01" ? { id: extension.id, status: "experimental", evidenceUrl: `https://github.com/stacksjs/stacks/blob/${revision}/protocol/evidence/desktop-support.json`, notes: "Craft targets are explicitly experimental pending native release evidence and signing infrastructure." } : { id: extension.id, status: "unsupported", evidenceUrl: null, notes: "Not evaluated by this adapter." }),
|
|
334
|
+
exceptions: [],
|
|
335
|
+
generator: { name: "@stacksjs/protocol-adapter", version: "1.0.0-draft.1", revision }
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
export async function run() {
|
|
339
|
+
const report = await buildReport(), schema = JSON.parse(readFileSync(resolve(suiteRoot, "schemas/conformance-report.schema.json"), "utf8")), catalog = JSON.parse(readFileSync(resolve(suiteRoot, "catalog.json"), "utf8")), fixtures = JSON.parse(readFileSync(resolve(suiteRoot, "fixtures/conformance.json"), "utf8")), ajv = new Ajv2020({ allErrors: !0, strict: !1 });
|
|
340
|
+
addFormats(ajv);
|
|
341
|
+
const validate = ajv.compile(schema), errors = [
|
|
342
|
+
...validate(report) ? [] : (validate.errors || []).map((error) => `${error.instancePath || "/"} ${error.message || "is invalid"}`),
|
|
343
|
+
...validateSemantics(report, catalog, fixtures)
|
|
344
|
+
];
|
|
345
|
+
if (errors.length > 0) {
|
|
346
|
+
for (const error of errors)
|
|
347
|
+
console.error(`error: ${error}`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
const outputDirectory = resolve(process.env.PROTOCOL_REPORT_DIR || resolve(root, ".github/protocol/reports"));
|
|
351
|
+
mkdirSync(outputDirectory, { recursive: !0 });
|
|
352
|
+
writeFileSync(resolve(outputDirectory, "stacks-conformance.json"), `${JSON.stringify(report, null, 2)}
|
|
353
|
+
`);
|
|
354
|
+
writeFileSync(resolve(outputDirectory, "stacks-conformance.md"), renderSummary(report));
|
|
355
|
+
console.log(`Protocol report generated in ${outputDirectory}`);
|
|
356
|
+
}
|
|
357
|
+
if (import.meta.main)
|
|
358
|
+
await run();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { buildReport, executeConfigEvidence, executeConventionsEvidence, executeDatabaseEvidence, executeLifecycleEvidence, executeQueryEvidence, executeSecurityEvidence, executeValidationEvidence } from "./run-conformance";
|
|
3
|
+
const REVISION = "0".repeat(40);
|
|
4
|
+
describe("protocol adapter evidence", () => {
|
|
5
|
+
it("executes authenticated encryption, timing-safe comparison, and default escaping checks", () => {
|
|
6
|
+
const evidence = executeSecurityEvidence(REVISION);
|
|
7
|
+
expect(evidence.get("CORE-SEC-05")?.status).toBe("pass");
|
|
8
|
+
expect(evidence.get("CORE-SEC-07")?.status).toBe("pass");
|
|
9
|
+
expect(evidence.get("CORE-SEC-02")?.status).toBe("pass");
|
|
10
|
+
});
|
|
11
|
+
it("rejects unavailable and planned drivers loudly, before any work begins", () => {
|
|
12
|
+
const evidence = executeConfigEvidence(REVISION);
|
|
13
|
+
expect(evidence.get("CORE-CONFIG-02")?.status).toBe("pass");
|
|
14
|
+
expect(evidence.get("STD-DRV-01")?.status).toBe("pass");
|
|
15
|
+
});
|
|
16
|
+
it("invokes an Action directly, independent of any HTTP transport", async () => {
|
|
17
|
+
const evidence = await executeLifecycleEvidence(REVISION);
|
|
18
|
+
expect(evidence.get("CORE-MVA-01")?.status).toBe("pass");
|
|
19
|
+
});
|
|
20
|
+
it("parameterizes untrusted values instead of inlining them into SQL", async () => {
|
|
21
|
+
const evidence = await executeQueryEvidence(REVISION);
|
|
22
|
+
expect(evidence.get("CORE-SEC-01")?.status).toBe("pass");
|
|
23
|
+
});
|
|
24
|
+
it("performs a CRUD round-trip, a relation join, and rolls back a failed transaction", async () => {
|
|
25
|
+
const evidence = await executeDatabaseEvidence(REVISION);
|
|
26
|
+
expect(evidence.get("CORE-DATA-01")?.status).toBe("pass");
|
|
27
|
+
expect(evidence.get("CORE-DATA-02")?.status).toBe("pass");
|
|
28
|
+
expect(evidence.get("CORE-DATA-03")?.status).toBe("pass");
|
|
29
|
+
});
|
|
30
|
+
it("produces field-keyed validation errors and a redacted 422 envelope", async () => {
|
|
31
|
+
const evidence = await executeValidationEvidence(REVISION);
|
|
32
|
+
expect(evidence.get("CORE-VAL-01")?.status).toBe("pass");
|
|
33
|
+
expect(evidence.get("CORE-ERR-01")?.status).toBe("pass");
|
|
34
|
+
expect(evidence.get("CORE-ERR-02")?.status).toBe("pass");
|
|
35
|
+
});
|
|
36
|
+
it("maps canonical roles to paths and prefers app-owned overrides", () => {
|
|
37
|
+
const evidence = executeConventionsEvidence(REVISION);
|
|
38
|
+
expect(evidence.get("CORE-CONV-01")?.status).toBe("pass");
|
|
39
|
+
expect(evidence.get("CORE-CONV-02")?.status).toBe("pass");
|
|
40
|
+
});
|
|
41
|
+
it("reports each registered driver independently", async () => {
|
|
42
|
+
const report = await buildReport();
|
|
43
|
+
expect(report.drivers.length).toBeGreaterThan(7);
|
|
44
|
+
expect(report.drivers).toContainEqual(expect.objectContaining({ category: "database", name: "sqlite" }));
|
|
45
|
+
expect(report.drivers).toContainEqual(expect.objectContaining({ category: "queue", name: "sqs" }));
|
|
46
|
+
});
|
|
47
|
+
it("marks every passing requirement with a source evidence url", async () => {
|
|
48
|
+
const passing = (await buildReport()).results.filter((result) => result.status === "pass");
|
|
49
|
+
expect(passing.length).toBeGreaterThanOrEqual(15);
|
|
50
|
+
expect(passing.every((result) => typeof result.evidenceUrl === "string" && result.evidenceUrl.startsWith("https://"))).toBe(!0);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CapabilityDriver } from '../../../../config/src/capabilities';
|
|
2
|
+
export declare function planDriverContracts(drivers: readonly CapabilityDriver[]): {
|
|
3
|
+
tests: string[]
|
|
4
|
+
records: Array<CapabilityDriver & { execution: 'run' | 'not-run', reason: string | null }>
|
|
5
|
+
};
|
|
6
|
+
export declare function runDriverContracts(): void;
|
|
7
|
+
export declare function run(): Promise<void>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { arch, platform } from "node:os";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { capabilityRegistry } from "../../../../config/src/capabilities";
|
|
6
|
+
const root = resolve(import.meta.dir, "../../../../../../.."), reportRoot = resolve(process.env.PROTOCOL_REPORT_DIR || resolve(root, ".github/protocol/reports"));
|
|
7
|
+
export function planDriverContracts(drivers) {
|
|
8
|
+
const tests = [...new Set(drivers.filter((driver) => driver.status === "supported").flatMap((driver) => driver.testEvidence))].sort(), records = drivers.map((driver) => ({
|
|
9
|
+
...driver,
|
|
10
|
+
execution: driver.status === "supported" ? "run" : "not-run",
|
|
11
|
+
reason: driver.status === "supported" ? null : `Maturity is ${driver.status}; prerequisites and missing evidence are reported without promotion.`
|
|
12
|
+
}));
|
|
13
|
+
return { tests, records };
|
|
14
|
+
}
|
|
15
|
+
function sqliteVersion() {
|
|
16
|
+
const database = new Database(":memory:");
|
|
17
|
+
try {
|
|
18
|
+
return database.query("select sqlite_version() as version").get().version;
|
|
19
|
+
} finally {
|
|
20
|
+
database.close();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function markdown(report) {
|
|
24
|
+
const rows = report.drivers.map((driver) => `| ${driver.category} | ${driver.name} | ${driver.maturity} | ${driver.execution} | ${driver.topology} | ${driver.reason || "Retained test paths passed."} |`).join(`
|
|
25
|
+
`);
|
|
26
|
+
return `# Stacks driver contract matrix
|
|
27
|
+
|
|
28
|
+
- Source: \`${report.sourceRevision}\`
|
|
29
|
+
- Runtime: Bun \`${report.environment.runtime.version}\` on \`${report.environment.platform.os}/${report.environment.platform.architecture}\`
|
|
30
|
+
- SQLite: \`${report.environment.services.sqlite}\`
|
|
31
|
+
- Redis: \`${report.environment.services.redis.version}\` at \`${report.environment.services.redis.topology}\`
|
|
32
|
+
- Test files: ${report.testRun.files.length}
|
|
33
|
+
- Result: **${report.testRun.status}**
|
|
34
|
+
|
|
35
|
+
| Category | Driver | Maturity | Execution | Topology | Evidence boundary |
|
|
36
|
+
| --- | --- | --- | --- | --- | --- |
|
|
37
|
+
${rows}
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
export function runDriverContracts() {
|
|
41
|
+
const plan = planDriverContracts(capabilityRegistry);
|
|
42
|
+
if (!plan.tests.length)
|
|
43
|
+
throw Error("No supported driver contracts were selected");
|
|
44
|
+
const revisionResult = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: root });
|
|
45
|
+
if (revisionResult.exitCode !== 0)
|
|
46
|
+
throw Error("Could not resolve the Stacks revision");
|
|
47
|
+
const started = performance.now(), result = Bun.spawnSync(["bun", "test", ...plan.tests], { cwd: root, stdout: "inherit", stderr: "inherit" }), durationMs = Math.round(performance.now() - started), status = result.exitCode === 0 ? "pass" : "fail", report = {
|
|
48
|
+
schemaVersion: "1.0.0",
|
|
49
|
+
sourceRevision: revisionResult.stdout.toString().trim(),
|
|
50
|
+
environment: {
|
|
51
|
+
runtime: { name: "bun", version: Bun.version },
|
|
52
|
+
platform: { os: platform(), architecture: arch() },
|
|
53
|
+
ci: process.env.GITHUB_ACTIONS === "true" ? "github-actions" : "local",
|
|
54
|
+
services: {
|
|
55
|
+
sqlite: sqliteVersion(),
|
|
56
|
+
redis: {
|
|
57
|
+
version: process.env.REDIS_SERVICE_VERSION || "unreported",
|
|
58
|
+
url: process.env.REDIS_URL ? "redis://127.0.0.1:6379/0" : "not provisioned",
|
|
59
|
+
topology: "single-node loopback process provisioned by Pantry Action"
|
|
60
|
+
},
|
|
61
|
+
otherExternal: "not provisioned; partial/experimental drivers are not promoted"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
testRun: { status, durationMs, files: plan.tests },
|
|
65
|
+
drivers: plan.records.map((driver) => ({
|
|
66
|
+
category: driver.category,
|
|
67
|
+
name: driver.name,
|
|
68
|
+
maturity: driver.status,
|
|
69
|
+
execution: driver.execution === "run" ? status : driver.execution,
|
|
70
|
+
topology: driver.topology,
|
|
71
|
+
prerequisites: driver.prerequisites,
|
|
72
|
+
evidence: driver.testEvidence,
|
|
73
|
+
limitations: driver.limitations,
|
|
74
|
+
reason: driver.reason
|
|
75
|
+
}))
|
|
76
|
+
};
|
|
77
|
+
mkdirSync(reportRoot, { recursive: !0 });
|
|
78
|
+
writeFileSync(resolve(reportRoot, "driver-matrix.json"), `${JSON.stringify(report, null, 2)}
|
|
79
|
+
`);
|
|
80
|
+
writeFileSync(resolve(reportRoot, "driver-matrix.md"), markdown(report));
|
|
81
|
+
if (status === "fail")
|
|
82
|
+
throw Error("Supported driver contract tests failed");
|
|
83
|
+
console.log(`Driver contract matrix passed ${plan.tests.length} retained test files`);
|
|
84
|
+
}
|
|
85
|
+
export async function run() {
|
|
86
|
+
try {
|
|
87
|
+
runDriverContracts();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (import.meta.main)
|
|
94
|
+
await run();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { planDriverContracts } from "./run-driver-contracts";
|
|
3
|
+
describe("driver contract matrix planning", () => {
|
|
4
|
+
it("runs only supported evidence and reports all other maturity states explicitly", () => {
|
|
5
|
+
const plan = planDriverContracts([
|
|
6
|
+
{ category: "cache", name: "memory", status: "supported", implementation: "memory.ts", testEvidence: ["memory.test.ts"], topology: "memory", prerequisites: [], limitations: [] },
|
|
7
|
+
{ category: "cache", name: "redis", status: "partial", implementation: "redis.ts", testEvidence: ["redis.test.ts"], topology: "redis", prerequisites: ["Redis"], limitations: ["No live evidence"] },
|
|
8
|
+
{ category: "queue", name: "sqs", status: "unsupported", implementation: null, testEvidence: [], topology: "sqs", prerequisites: ["AWS"], limitations: ["Reserved"] }
|
|
9
|
+
]);
|
|
10
|
+
expect(plan.tests).toEqual(["memory.test.ts"]);
|
|
11
|
+
expect(plan.records.map((record) => [record.name, record.execution])).toEqual([
|
|
12
|
+
["memory", "run"],
|
|
13
|
+
["redis", "not-run"],
|
|
14
|
+
["sqs", "not-run"]
|
|
15
|
+
]);
|
|
16
|
+
expect(plan.records[1].reason).toContain("partial");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare function buildSourceManifest(requestedRevision: string): SourceManifest;
|
|
2
|
+
export declare function run(): Promise<void>;
|
|
3
|
+
declare interface PackageEntry { name: string, path: string, version: string, publishable: boolean }
|
|
4
|
+
declare interface SourceManifest {
|
|
5
|
+
schemaVersion: '1.0.0'
|
|
6
|
+
repository: string
|
|
7
|
+
sourceRevision: string
|
|
8
|
+
sourceTree: string
|
|
9
|
+
sourceDigest: string
|
|
10
|
+
generatedAt: string
|
|
11
|
+
classification: {
|
|
12
|
+
status: 'reference-implementation'
|
|
13
|
+
conformance: 'unverified'
|
|
14
|
+
notes: string
|
|
15
|
+
}
|
|
16
|
+
prerequisites: Record<string, string>
|
|
17
|
+
packages: PackageEntry[]
|
|
18
|
+
inventory: {
|
|
19
|
+
totalFiles: number
|
|
20
|
+
totalBytes: number
|
|
21
|
+
categories: Record<string, { files: number, bytes: number }>
|
|
22
|
+
extensions: Record<string, number>
|
|
23
|
+
}
|
|
24
|
+
protocolSuite: { version: string, rfcsRevision: string }
|
|
25
|
+
}
|