@sleepy-hollow/framework 0.3.4 → 0.3.5
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/CHANGELOG.md +10 -0
- package/dist/{chunk-RYKRRAPD.js → chunk-YBEJCNGD.js} +16 -2
- package/dist/chunk-YBEJCNGD.js.map +1 -0
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +338 -42
- package/dist/cli.js.map +1 -1
- package/dist/testing.d.ts +13 -3
- package/dist/testing.js +3 -1
- package/dist/{types-Bet36nZS.d.ts → types-t92f8Gqs.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-RYKRRAPD.js.map +0 -1
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { R as RequirementEvidence, k as RequirementDependency,
|
|
2
|
+
import { R as RequirementEvidence, k as RequirementDependency, g as TestManifest, h as TestExecutionResult } from './types-t92f8Gqs.js';
|
|
3
3
|
import './types-BC7LJJ6G.js';
|
|
4
4
|
import './types-BUXw3UwN.js';
|
|
5
5
|
import 'drizzle-orm/better-sqlite3';
|
|
@@ -168,6 +168,8 @@ interface TestIsolationPolicy {
|
|
|
168
168
|
}
|
|
169
169
|
interface TestCommandInventory {
|
|
170
170
|
readonly captureArtifactPath?: string;
|
|
171
|
+
readonly manifestPath?: string;
|
|
172
|
+
readonly resultsPath?: string;
|
|
171
173
|
readonly projectRootDisplay: string;
|
|
172
174
|
readonly requirements: readonly RequirementEvidence[];
|
|
173
175
|
readonly dependencyGraph: readonly RequirementDependency[];
|
|
@@ -307,7 +309,7 @@ interface CliDependencies {
|
|
|
307
309
|
*/
|
|
308
310
|
|
|
309
311
|
/** Version of the CLI, reported by `hollow --version`. */
|
|
310
|
-
declare const VERSION = "0.3.
|
|
312
|
+
declare const VERSION = "0.3.5";
|
|
311
313
|
/**
|
|
312
314
|
* Runs one CLI invocation against caller-supplied I/O.
|
|
313
315
|
*
|
package/dist/cli.js
CHANGED
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
resolveConfiguration
|
|
4
4
|
} from "./chunk-2GNHTFCZ.js";
|
|
5
5
|
import {
|
|
6
|
+
CRITERION_TEST_DISCOVERY,
|
|
7
|
+
createTestManifest,
|
|
6
8
|
createTraceabilityReport,
|
|
7
9
|
selectAffectedTests
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-YBEJCNGD.js";
|
|
9
11
|
import "./chunk-BAKXP7IR.js";
|
|
10
12
|
import {
|
|
11
13
|
composeProjectSecurity
|
|
@@ -22,7 +24,7 @@ import "./chunk-5WRI5ZAA.js";
|
|
|
22
24
|
|
|
23
25
|
// cli/main.ts
|
|
24
26
|
import { realpathSync } from "fs";
|
|
25
|
-
import { pathToFileURL as
|
|
27
|
+
import { pathToFileURL as pathToFileURL5 } from "url";
|
|
26
28
|
|
|
27
29
|
// cli/adapters.ts
|
|
28
30
|
import { isAbsolute as isAbsolute5, join as join4, relative as relative5, sep as sep6 } from "path";
|
|
@@ -1006,7 +1008,7 @@ var CreationError = class extends Error {
|
|
|
1006
1008
|
};
|
|
1007
1009
|
|
|
1008
1010
|
// cli/create/create.ts
|
|
1009
|
-
var VERSION = "0.3.
|
|
1011
|
+
var VERSION = "0.3.5";
|
|
1010
1012
|
var NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
1011
1013
|
function files(name) {
|
|
1012
1014
|
return {
|
|
@@ -1142,6 +1144,12 @@ owners:
|
|
|
1142
1144
|
|
|
1143
1145
|
Use the official Sleepy Hollow skill to replace this planning placeholder with
|
|
1144
1146
|
the complete application requirements before generating endpoints.
|
|
1147
|
+
|
|
1148
|
+
## Cross-cutting acceptance criteria
|
|
1149
|
+
|
|
1150
|
+
- AC-APP-PLACEHOLDER-001: The application requirements are completed and approved before implementation.
|
|
1151
|
+
|
|
1152
|
+
## Governance record
|
|
1145
1153
|
`,
|
|
1146
1154
|
"sleepyhollow.config.ts": `import { defineProject } from "./.sleepyhollow/project.ts";
|
|
1147
1155
|
|
|
@@ -1173,7 +1181,7 @@ async function pathExists(path) {
|
|
|
1173
1181
|
throw error;
|
|
1174
1182
|
}
|
|
1175
1183
|
}
|
|
1176
|
-
var FRAMEWORK_VERSION = "0.3.
|
|
1184
|
+
var FRAMEWORK_VERSION = "0.3.5";
|
|
1177
1185
|
async function createProject(options) {
|
|
1178
1186
|
if (!NAME.test(options.name) || options.name.length > 64) {
|
|
1179
1187
|
throw creationError(
|
|
@@ -1201,10 +1209,10 @@ async function createProject(options) {
|
|
|
1201
1209
|
);
|
|
1202
1210
|
try {
|
|
1203
1211
|
await platform.mkdir(staging);
|
|
1204
|
-
for (const
|
|
1205
|
-
const target = join2(staging,
|
|
1212
|
+
for (const relative8 of createdFiles) {
|
|
1213
|
+
const target = join2(staging, relative8);
|
|
1206
1214
|
await platform.mkdir(resolve(target, ".."), { recursive: true });
|
|
1207
|
-
await platform.writeTextFile(target, contents[
|
|
1215
|
+
await platform.writeTextFile(target, contents[relative8], { createNew: true });
|
|
1208
1216
|
}
|
|
1209
1217
|
await platform.rename(staging, destination);
|
|
1210
1218
|
} catch (error) {
|
|
@@ -2969,7 +2977,7 @@ function renderArtifacts(inventory2) {
|
|
|
2969
2977
|
];
|
|
2970
2978
|
const manifestContent = canonicalJson({
|
|
2971
2979
|
schema: "sleepy-hollow-generated-manifest/v1",
|
|
2972
|
-
generatorVersion: "0.3.
|
|
2980
|
+
generatorVersion: "0.3.5",
|
|
2973
2981
|
serviceId: normalized.serviceId,
|
|
2974
2982
|
inputDigest: digest2(input),
|
|
2975
2983
|
artifacts: Object.fromEntries(
|
|
@@ -3243,7 +3251,7 @@ function inventoryFromRoutes(routes2, options) {
|
|
|
3243
3251
|
return {
|
|
3244
3252
|
serviceId: options.serviceId,
|
|
3245
3253
|
title: options.title ?? options.serviceId,
|
|
3246
|
-
version: options.version ?? "0.3.
|
|
3254
|
+
version: options.version ?? "0.3.5",
|
|
3247
3255
|
...options.description ? { description: options.description } : {},
|
|
3248
3256
|
operations,
|
|
3249
3257
|
securitySchemes: options.securitySchemes ?? {}
|
|
@@ -3474,7 +3482,7 @@ function invocation(plan2, options) {
|
|
|
3474
3482
|
const args = [
|
|
3475
3483
|
"./node_modules/vitest/vitest.mjs",
|
|
3476
3484
|
"run",
|
|
3477
|
-
"--reporter=
|
|
3485
|
+
"--reporter=tap-flat",
|
|
3478
3486
|
...plan2.filter ? ["--testNamePattern", plan2.filter] : [],
|
|
3479
3487
|
...files2
|
|
3480
3488
|
];
|
|
@@ -3522,11 +3530,16 @@ function parseTap(source, inventory2, failureEvidence) {
|
|
|
3522
3530
|
);
|
|
3523
3531
|
const events = [];
|
|
3524
3532
|
for (const line of lines) {
|
|
3525
|
-
const matched = line.match(
|
|
3533
|
+
const matched = line.match(
|
|
3534
|
+
/^(ok|not ok) \d+ - (.*?)(?:\s+#\s+(SKIP)\b.*|\s+#\s+.*)?$/i
|
|
3535
|
+
);
|
|
3526
3536
|
if (!matched) continue;
|
|
3527
|
-
const
|
|
3537
|
+
const rawName = matched[2];
|
|
3538
|
+
const separator = rawName.indexOf(" > ");
|
|
3539
|
+
const file = separator >= 0 ? rawName.slice(0, separator) : byName.get(rawName) ?? "<runner>";
|
|
3540
|
+
const name = separator >= 0 ? rawName.slice(separator + 3) : rawName;
|
|
3528
3541
|
events.push({
|
|
3529
|
-
file
|
|
3542
|
+
file,
|
|
3530
3543
|
name,
|
|
3531
3544
|
status: matched[3]?.toUpperCase() === "SKIP" ? "skipped" : matched[1] === "ok" ? "passed" : "failed",
|
|
3532
3545
|
...matched[1] === "not ok" && failureEvidence ? { evidence: failureEvidence } : {}
|
|
@@ -3887,6 +3900,32 @@ function renderJson(result) {
|
|
|
3887
3900
|
}
|
|
3888
3901
|
|
|
3889
3902
|
// cli/test/command.ts
|
|
3903
|
+
async function persistEvidence(inventory2, result) {
|
|
3904
|
+
if (!inventory2.manifestPath || !inventory2.resultsPath) return;
|
|
3905
|
+
const results = result.tests.filter((test) => !test.id.startsWith("unmapped:")).map((test) => ({
|
|
3906
|
+
testId: test.id,
|
|
3907
|
+
status: test.status === "unmapped" ? "failed" : test.status,
|
|
3908
|
+
...test.durationMs === null ? {} : { durationMs: test.durationMs },
|
|
3909
|
+
...test.evidence ? { evidence: test.evidence } : {}
|
|
3910
|
+
}));
|
|
3911
|
+
await platform.mkdir(inventory2.manifestPath.slice(
|
|
3912
|
+
0,
|
|
3913
|
+
inventory2.manifestPath.lastIndexOf("/")
|
|
3914
|
+
), { recursive: true });
|
|
3915
|
+
await platform.writeTextFile(
|
|
3916
|
+
inventory2.manifestPath,
|
|
3917
|
+
`${JSON.stringify(inventory2.manifest, null, 2)}
|
|
3918
|
+
`
|
|
3919
|
+
);
|
|
3920
|
+
await platform.writeTextFile(
|
|
3921
|
+
inventory2.resultsPath,
|
|
3922
|
+
`${JSON.stringify({
|
|
3923
|
+
schema: "sleepy-hollow-test-results/v1",
|
|
3924
|
+
results
|
|
3925
|
+
}, null, 2)}
|
|
3926
|
+
`
|
|
3927
|
+
);
|
|
3928
|
+
}
|
|
3890
3929
|
function parse3(args) {
|
|
3891
3930
|
let json2 = false;
|
|
3892
3931
|
let scope = { kind: "full" };
|
|
@@ -4012,7 +4051,7 @@ async function execute(args, io, load, runner) {
|
|
|
4012
4051
|
} catch {
|
|
4013
4052
|
return emit(loadFailure(parsed.scope), parsed.json, io);
|
|
4014
4053
|
}
|
|
4015
|
-
const capturePath = `${io.cwd}/generated/capture.json`;
|
|
4054
|
+
const capturePath = inventory2.captureArtifactPath ?? `${io.cwd}/generated/capture.json`;
|
|
4016
4055
|
inventory2 = { ...inventory2, captureArtifactPath: capturePath };
|
|
4017
4056
|
const testPlan = plan(inventory2, parsed.scope);
|
|
4018
4057
|
if (testPlan.diagnostics.some((item) => item.severity === "error")) {
|
|
@@ -4064,6 +4103,26 @@ async function execute(args, io, load, runner) {
|
|
|
4064
4103
|
};
|
|
4065
4104
|
}
|
|
4066
4105
|
const normalized = normalizeRunnerResult(testPlan, inventory2, runnerResult);
|
|
4106
|
+
let persistedResult = normalized;
|
|
4107
|
+
if (inventory2.manifestPath && inventory2.resultsPath) {
|
|
4108
|
+
try {
|
|
4109
|
+
await persistEvidence(inventory2, normalized);
|
|
4110
|
+
} catch {
|
|
4111
|
+
persistedResult = {
|
|
4112
|
+
...normalized,
|
|
4113
|
+
ok: false,
|
|
4114
|
+
diagnostics: [
|
|
4115
|
+
...normalized.diagnostics,
|
|
4116
|
+
{
|
|
4117
|
+
code: "SH_TEST_EVIDENCE_NOT_PERSISTED",
|
|
4118
|
+
severity: "error",
|
|
4119
|
+
summary: "The test run completed but governed test evidence could not be persisted.",
|
|
4120
|
+
correction: "Repair the generated directory and rerun hollow test before checking the project."
|
|
4121
|
+
}
|
|
4122
|
+
]
|
|
4123
|
+
};
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4067
4126
|
let persisted = true;
|
|
4068
4127
|
try {
|
|
4069
4128
|
await platform.stat(capturePath);
|
|
@@ -4071,8 +4130,8 @@ async function execute(args, io, load, runner) {
|
|
|
4071
4130
|
persisted = false;
|
|
4072
4131
|
}
|
|
4073
4132
|
return emit(
|
|
4074
|
-
persisted ?
|
|
4075
|
-
...
|
|
4133
|
+
persisted ? persistedResult : {
|
|
4134
|
+
...persistedResult,
|
|
4076
4135
|
diagnostics: [
|
|
4077
4136
|
...normalized.diagnostics,
|
|
4078
4137
|
{
|
|
@@ -4097,7 +4156,7 @@ var CLI_COMMANDS = [
|
|
|
4097
4156
|
"generate",
|
|
4098
4157
|
"deploy"
|
|
4099
4158
|
];
|
|
4100
|
-
var CLI_VERSION = "0.3.
|
|
4159
|
+
var CLI_VERSION = "0.3.5";
|
|
4101
4160
|
var commandMetadata = {
|
|
4102
4161
|
create: {
|
|
4103
4162
|
description: "Create one deterministic Sleepy Hollow project.",
|
|
@@ -5227,10 +5286,11 @@ function digestOf(source, path) {
|
|
|
5227
5286
|
}
|
|
5228
5287
|
function approvalOf(source, digest3, criteria) {
|
|
5229
5288
|
const governance = source.slice(source.indexOf("## Governance record"));
|
|
5230
|
-
const
|
|
5231
|
-
|
|
5289
|
+
const approvals = [...governance.matchAll(/^### Approval[^\r\n]*\r?$/gm)];
|
|
5290
|
+
const latest = approvals.at(-1);
|
|
5291
|
+
if (!latest || latest.index === void 0) return void 0;
|
|
5232
5292
|
const approvalRemainder = governance.slice(
|
|
5233
|
-
|
|
5293
|
+
latest.index + latest[0].length
|
|
5234
5294
|
);
|
|
5235
5295
|
const next = approvalRemainder.search(/^### /m);
|
|
5236
5296
|
const approval = next < 0 ? approvalRemainder : approvalRemainder.slice(0, next);
|
|
@@ -5239,7 +5299,7 @@ function approvalOf(source, digest3, criteria) {
|
|
|
5239
5299
|
const approvedAt = /- Approved at:\s*(.+?)\.?(?:\r?\n|$)/.exec(approval)?.[1]?.replace(/\.$/, "");
|
|
5240
5300
|
const criterionText = /- Approved criteria:\s*(.+?)\.?(?:\r?\n|$)/.exec(approval)?.[1] ?? "";
|
|
5241
5301
|
const recordedDigest = /sha256:([a-f0-9]{64})/.exec(approval)?.[1];
|
|
5242
|
-
const decisionSource = /- Decision source:\s*([\s\S]*?)(?=\r?\n- [A-Z]|$)/.exec(approval)?.[1]?.replace(/\r?\n\s+/g, " ").replace(/\.$/, "");
|
|
5302
|
+
const decisionSource = /- Decision source:\s*([\s\S]*?)(?=\r?\n- [A-Z]|$)/.exec(approval)?.[1]?.replace(/\r?\n\s+/g, " ").trim().replace(/\.$/, "");
|
|
5243
5303
|
const approvedCriteria = criteria.filter(
|
|
5244
5304
|
(item) => criterionText.includes(item.id)
|
|
5245
5305
|
).map((item) => item.id);
|
|
@@ -5255,6 +5315,18 @@ function approvalOf(source, digest3, criteria) {
|
|
|
5255
5315
|
valid
|
|
5256
5316
|
};
|
|
5257
5317
|
}
|
|
5318
|
+
function redStateValid(source) {
|
|
5319
|
+
const governance = source.slice(source.indexOf("## Governance record"));
|
|
5320
|
+
const records = [
|
|
5321
|
+
...governance.matchAll(/^### Red-state evidence[^\r\n]*\r?$/gm)
|
|
5322
|
+
];
|
|
5323
|
+
const latest = records.at(-1);
|
|
5324
|
+
if (!latest || latest.index === void 0) return false;
|
|
5325
|
+
const remainder = governance.slice(latest.index + latest[0].length);
|
|
5326
|
+
const next = remainder.search(/^### /m);
|
|
5327
|
+
const record = next < 0 ? remainder : remainder.slice(0, next);
|
|
5328
|
+
return /- Status:\s*(?:credible red state captured|failed as expected)\./i.test(record) && /- (?:Base revision|Observed at):\s*\S+/i.test(record) && /- Result:\s*\S+/i.test(record);
|
|
5329
|
+
}
|
|
5258
5330
|
function parseRequirementDocument(source, path, kind) {
|
|
5259
5331
|
const { metadata, body, bodyOffset } = frontmatterOf(source, path);
|
|
5260
5332
|
const diagnostics = [];
|
|
@@ -5435,7 +5507,8 @@ function parseRequirementDocument(source, path, kind) {
|
|
|
5435
5507
|
criteria: criterionResult.criteria,
|
|
5436
5508
|
sections,
|
|
5437
5509
|
governedContentDigest,
|
|
5438
|
-
approval
|
|
5510
|
+
approval,
|
|
5511
|
+
redStateValid: redStateValid(source)
|
|
5439
5512
|
};
|
|
5440
5513
|
}
|
|
5441
5514
|
|
|
@@ -5448,7 +5521,7 @@ function parseRequirement(source, path, kind) {
|
|
|
5448
5521
|
function issue2(code, path, summary, correction, line) {
|
|
5449
5522
|
return { code, path, summary, correction, ...line ? { line } : {} };
|
|
5450
5523
|
}
|
|
5451
|
-
async function collect(absoluteRoot, displayRoot, serviceId2, options) {
|
|
5524
|
+
async function collect(absoluteRoot, displayRoot, serviceId2, kind, options) {
|
|
5452
5525
|
const list = options.listDirectory ?? (async (path) => {
|
|
5453
5526
|
const entries = [];
|
|
5454
5527
|
for await (const entry of platform.readDir(path)) {
|
|
@@ -5480,6 +5553,7 @@ async function collect(absoluteRoot, displayRoot, serviceId2, options) {
|
|
|
5480
5553
|
path: nextDisplay,
|
|
5481
5554
|
absolutePath: nextAbsolute,
|
|
5482
5555
|
legacy: entry.name === "requirements.md",
|
|
5556
|
+
kind,
|
|
5483
5557
|
...serviceId2 ? { serviceId: serviceId2 } : {}
|
|
5484
5558
|
});
|
|
5485
5559
|
}
|
|
@@ -5500,16 +5574,45 @@ async function requirements(project, options) {
|
|
|
5500
5574
|
serviceId: void 0
|
|
5501
5575
|
}];
|
|
5502
5576
|
const files2 = [];
|
|
5577
|
+
files2.push({
|
|
5578
|
+
path: project.requirementsFile,
|
|
5579
|
+
absolutePath: `${project.projectRoot}/${project.requirementsFile}`,
|
|
5580
|
+
legacy: project.requirementsFile.endsWith("requirements.md"),
|
|
5581
|
+
kind: "application"
|
|
5582
|
+
});
|
|
5583
|
+
for (const service of project.services) {
|
|
5584
|
+
const absolutePath = `${project.projectRoot}/${service.requirementsPath}`;
|
|
5585
|
+
try {
|
|
5586
|
+
await platform.lstat(absolutePath);
|
|
5587
|
+
files2.push({
|
|
5588
|
+
path: service.requirementsPath,
|
|
5589
|
+
absolutePath,
|
|
5590
|
+
legacy: service.requirementsPath.endsWith("requirements.md"),
|
|
5591
|
+
kind: "application"
|
|
5592
|
+
});
|
|
5593
|
+
} catch (error) {
|
|
5594
|
+
if (!platform.isNotFound(error)) throw error;
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5503
5597
|
for (const root of roots) {
|
|
5504
5598
|
files2.push(
|
|
5505
|
-
...await collect(
|
|
5599
|
+
...await collect(
|
|
5600
|
+
root.absolute,
|
|
5601
|
+
root.display,
|
|
5602
|
+
root.serviceId,
|
|
5603
|
+
"endpoint",
|
|
5604
|
+
options
|
|
5605
|
+
)
|
|
5506
5606
|
);
|
|
5507
5607
|
}
|
|
5508
|
-
|
|
5608
|
+
const uniqueFiles = [...new Map(
|
|
5609
|
+
files2.map((file) => [file.absolutePath, file])
|
|
5610
|
+
).values()];
|
|
5611
|
+
uniqueFiles.sort((left, right) => left.path.localeCompare(right.path));
|
|
5509
5612
|
const diagnostics = [];
|
|
5510
5613
|
const loaded = [];
|
|
5511
5614
|
const seen = /* @__PURE__ */ new Map();
|
|
5512
|
-
for (const file of
|
|
5615
|
+
for (const file of uniqueFiles) {
|
|
5513
5616
|
if (file.legacy) {
|
|
5514
5617
|
diagnostics.push(issue2(
|
|
5515
5618
|
"SH_EVIDENCE_REQUIREMENT_LEGACY_FILENAME",
|
|
@@ -5533,7 +5636,7 @@ async function requirements(project, options) {
|
|
|
5533
5636
|
}
|
|
5534
5637
|
let parsed;
|
|
5535
5638
|
try {
|
|
5536
|
-
parsed = parseRequirement(source, file.path,
|
|
5639
|
+
parsed = parseRequirement(source, file.path, file.kind);
|
|
5537
5640
|
} catch (error) {
|
|
5538
5641
|
if (error instanceof PlanningError) {
|
|
5539
5642
|
for (const item of error.diagnostics) {
|
|
@@ -5585,7 +5688,14 @@ async function requirements(project, options) {
|
|
|
5585
5688
|
} : {},
|
|
5586
5689
|
path: file.path,
|
|
5587
5690
|
...file.serviceId ? { serviceId: file.serviceId } : {},
|
|
5588
|
-
approvalBound
|
|
5691
|
+
approvalBound,
|
|
5692
|
+
redStateValid: parsed.redStateValid,
|
|
5693
|
+
...file.kind === "endpoint" ? {
|
|
5694
|
+
routePath: typeof parsed.metadata.path === "string" ? parsed.metadata.path : void 0,
|
|
5695
|
+
methods: Array.isArray(parsed.metadata.methods) ? parsed.metadata.methods.filter(
|
|
5696
|
+
(method) => typeof method === "string"
|
|
5697
|
+
) : void 0
|
|
5698
|
+
} : {}
|
|
5589
5699
|
});
|
|
5590
5700
|
}
|
|
5591
5701
|
if (diagnostics.length > 0) throw new EvidenceError(diagnostics);
|
|
@@ -5602,12 +5712,15 @@ async function requirements(project, options) {
|
|
|
5602
5712
|
criteria: item.criteria,
|
|
5603
5713
|
...item.approval ? { approval: item.approval } : {},
|
|
5604
5714
|
path: item.path,
|
|
5605
|
-
redStateValid:
|
|
5715
|
+
redStateValid: item.redStateValid,
|
|
5716
|
+
...item.routePath ? { routePath: item.routePath } : {},
|
|
5717
|
+
...item.methods ? { methods: item.methods } : {}
|
|
5606
5718
|
}))
|
|
5607
5719
|
};
|
|
5608
5720
|
}
|
|
5609
5721
|
|
|
5610
5722
|
// cli/evidence/behavior.ts
|
|
5723
|
+
import { relative as relative6, sep as sep7 } from "path";
|
|
5611
5724
|
var CAPTURE_FILE = "capture.json";
|
|
5612
5725
|
async function capture(project, options) {
|
|
5613
5726
|
const path = `${project.projectRoot}/${project.generatedDirectory}/${CAPTURE_FILE}`;
|
|
@@ -5670,7 +5783,16 @@ function authenticationOf(security2) {
|
|
|
5670
5783
|
if (typeof declared !== "object" || declared === null) return "none";
|
|
5671
5784
|
return declared.mode === "required" ? "required" : "none";
|
|
5672
5785
|
}
|
|
5673
|
-
|
|
5786
|
+
function normalizedRequirementPath(path) {
|
|
5787
|
+
return path.replace(/\[([A-Za-z_][A-Za-z0-9_]*)\]/g, ":$1");
|
|
5788
|
+
}
|
|
5789
|
+
function ownerOf(route, requirements2) {
|
|
5790
|
+
const owners = requirements2.filter(
|
|
5791
|
+
(requirement) => requirement.routePath !== void 0 && normalizedRequirementPath(requirement.routePath) === route.path && requirement.methods?.includes(route.method)
|
|
5792
|
+
);
|
|
5793
|
+
return owners.length === 1 ? owners[0].id : void 0;
|
|
5794
|
+
}
|
|
5795
|
+
async function routes(project, artifact2, requirements2 = []) {
|
|
5674
5796
|
const roots = project.services.length > 0 ? project.services.map((service) => ({
|
|
5675
5797
|
absolute: `${project.projectRoot}/${service.apiRoot}`,
|
|
5676
5798
|
display: service.apiRoot
|
|
@@ -5703,10 +5825,12 @@ async function routes(project, artifact2) {
|
|
|
5703
5825
|
}
|
|
5704
5826
|
statuses2.add(request.responseStatus);
|
|
5705
5827
|
}
|
|
5828
|
+
const source = route.source.startsWith("/") ? relative6(root.absolute, route.source).split(sep7).join("/") : route.source;
|
|
5706
5829
|
found.push({
|
|
5830
|
+
requirementId: ownerOf(route, requirements2),
|
|
5707
5831
|
method: route.method,
|
|
5708
5832
|
path: route.path,
|
|
5709
|
-
source: `${root.display}
|
|
5833
|
+
source: `${root.display}/${source}`,
|
|
5710
5834
|
requestSchemaLocations: declaredLocations(route.operation.schemas),
|
|
5711
5835
|
requiredRequestLocations: [...readLocations].sort(),
|
|
5712
5836
|
responseSchemaStatuses: declaredStatuses(route.operation.schemas),
|
|
@@ -5721,11 +5845,172 @@ async function routes(project, artifact2) {
|
|
|
5721
5845
|
);
|
|
5722
5846
|
}
|
|
5723
5847
|
|
|
5848
|
+
// cli/evidence/tests.ts
|
|
5849
|
+
import { relative as relative7, sep as sep8 } from "path";
|
|
5850
|
+
import { pathToFileURL as pathToFileURL4 } from "url";
|
|
5851
|
+
var MANIFEST_FILE = "test-manifest.json";
|
|
5852
|
+
var RESULTS_FILE = "test-results.json";
|
|
5853
|
+
var TEST_FILE = /(?:_test|\.test)\.(?:ts|tsx|mts|cts)$/;
|
|
5854
|
+
var discoverySequence = 0;
|
|
5855
|
+
function relativeProjectPath(root, path) {
|
|
5856
|
+
return relative7(root, path).split(sep8).join("/");
|
|
5857
|
+
}
|
|
5858
|
+
async function collect2(root, projectRoot) {
|
|
5859
|
+
const found = [];
|
|
5860
|
+
const walk = async (directory) => {
|
|
5861
|
+
let entries;
|
|
5862
|
+
try {
|
|
5863
|
+
entries = [];
|
|
5864
|
+
for await (const entry of platform.readDir(directory)) entries.push(entry);
|
|
5865
|
+
} catch {
|
|
5866
|
+
return;
|
|
5867
|
+
}
|
|
5868
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
5869
|
+
for (const entry of entries) {
|
|
5870
|
+
if (entry.isSymlink) continue;
|
|
5871
|
+
const absolute = `${directory}/${entry.name}`;
|
|
5872
|
+
if (entry.isDirectory) {
|
|
5873
|
+
await walk(absolute);
|
|
5874
|
+
} else if (entry.isFile && TEST_FILE.test(entry.name)) {
|
|
5875
|
+
found.push({ absolute, relative: relativeProjectPath(projectRoot, absolute) });
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5878
|
+
};
|
|
5879
|
+
await walk(root);
|
|
5880
|
+
return found;
|
|
5881
|
+
}
|
|
5882
|
+
function testRoots(project) {
|
|
5883
|
+
const roots = project.services.length > 0 ? project.services.flatMap((service) => [
|
|
5884
|
+
`${project.projectRoot}/${service.testsRoot}`,
|
|
5885
|
+
`${project.projectRoot}/${service.apiRoot}`
|
|
5886
|
+
]) : [
|
|
5887
|
+
`${project.projectRoot}/tests`,
|
|
5888
|
+
`${project.projectRoot}/${project.apiDirectory}`
|
|
5889
|
+
];
|
|
5890
|
+
return [...new Set(roots)];
|
|
5891
|
+
}
|
|
5892
|
+
async function discoverFiles(project) {
|
|
5893
|
+
const files2 = (await Promise.all(testRoots(project).map(
|
|
5894
|
+
(root) => collect2(root, project.projectRoot)
|
|
5895
|
+
))).flat();
|
|
5896
|
+
const unique = new Map(files2.map((file) => [file.absolute, file]));
|
|
5897
|
+
return [...unique.values()].sort(
|
|
5898
|
+
(left, right) => left.relative.localeCompare(right.relative)
|
|
5899
|
+
);
|
|
5900
|
+
}
|
|
5901
|
+
async function discoverManifest(project) {
|
|
5902
|
+
const descriptors = [];
|
|
5903
|
+
const sources = {};
|
|
5904
|
+
const hook = {
|
|
5905
|
+
onRegistered: (descriptor) => descriptors.push(descriptor)
|
|
5906
|
+
};
|
|
5907
|
+
const registry = globalThis;
|
|
5908
|
+
registry[CRITERION_TEST_DISCOVERY] = hook;
|
|
5909
|
+
try {
|
|
5910
|
+
for (const file of await discoverFiles(project)) {
|
|
5911
|
+
const source = await platform.readTextFile(file.absolute);
|
|
5912
|
+
if (!/\bcriterionTest\s*\(/.test(source)) continue;
|
|
5913
|
+
sources[file.relative] = source;
|
|
5914
|
+
try {
|
|
5915
|
+
discoverySequence += 1;
|
|
5916
|
+
await import(`${pathToFileURL4(file.absolute).href}?sleepy_hollow_discovery=${discoverySequence}`);
|
|
5917
|
+
} catch (error) {
|
|
5918
|
+
throw new EvidenceError([{
|
|
5919
|
+
code: "SH_EVIDENCE_TEST_DISCOVERY_FAILED",
|
|
5920
|
+
path: file.relative,
|
|
5921
|
+
summary: error instanceof Error ? error.message : "A governed test could not be loaded for discovery.",
|
|
5922
|
+
correction: "Repair the criterionTest registration and its imports, then retry."
|
|
5923
|
+
}]);
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
} finally {
|
|
5927
|
+
delete registry[CRITERION_TEST_DISCOVERY];
|
|
5928
|
+
}
|
|
5929
|
+
return createTestManifest({ descriptors, sources });
|
|
5930
|
+
}
|
|
5931
|
+
function emptyManifest() {
|
|
5932
|
+
return { schema: "sleepy-hollow-test-manifest/v1", tests: [] };
|
|
5933
|
+
}
|
|
5934
|
+
function parseManifest(value, path) {
|
|
5935
|
+
if (!value || typeof value !== "object" || value.schema !== "sleepy-hollow-test-manifest/v1" || !Array.isArray(value.tests)) {
|
|
5936
|
+
throw new EvidenceError([{
|
|
5937
|
+
code: "SH_EVIDENCE_TEST_MANIFEST_INVALID",
|
|
5938
|
+
path,
|
|
5939
|
+
summary: "The persisted test manifest has an unsupported shape.",
|
|
5940
|
+
correction: "Regenerate test evidence with hollow test."
|
|
5941
|
+
}]);
|
|
5942
|
+
}
|
|
5943
|
+
return value;
|
|
5944
|
+
}
|
|
5945
|
+
function parseResults(value, path) {
|
|
5946
|
+
if (!value || typeof value !== "object" || value.schema !== "sleepy-hollow-test-results/v1" || !Array.isArray(value.results)) {
|
|
5947
|
+
throw new EvidenceError([{
|
|
5948
|
+
code: "SH_EVIDENCE_TEST_RESULTS_INVALID",
|
|
5949
|
+
path,
|
|
5950
|
+
summary: "The persisted test results have an unsupported shape.",
|
|
5951
|
+
correction: "Regenerate test evidence with hollow test."
|
|
5952
|
+
}]);
|
|
5953
|
+
}
|
|
5954
|
+
return value.results;
|
|
5955
|
+
}
|
|
5956
|
+
async function readJson(path) {
|
|
5957
|
+
let source;
|
|
5958
|
+
try {
|
|
5959
|
+
source = await platform.readTextFile(path);
|
|
5960
|
+
} catch (error) {
|
|
5961
|
+
if (platform.isNotFound(error)) return void 0;
|
|
5962
|
+
throw new EvidenceError([{
|
|
5963
|
+
code: "SH_EVIDENCE_TEST_ARTIFACT_UNREADABLE",
|
|
5964
|
+
path,
|
|
5965
|
+
summary: "A persisted test evidence artifact could not be read.",
|
|
5966
|
+
correction: "Repair the generated evidence permissions and retry."
|
|
5967
|
+
}]);
|
|
5968
|
+
}
|
|
5969
|
+
try {
|
|
5970
|
+
return JSON.parse(source);
|
|
5971
|
+
} catch {
|
|
5972
|
+
throw new EvidenceError([{
|
|
5973
|
+
code: "SH_EVIDENCE_TEST_ARTIFACT_INVALID",
|
|
5974
|
+
path,
|
|
5975
|
+
summary: "A persisted test evidence artifact is not valid JSON.",
|
|
5976
|
+
correction: "Regenerate test evidence with hollow test."
|
|
5977
|
+
}]);
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5980
|
+
async function readTestEvidence(project) {
|
|
5981
|
+
const generated = `${project.projectRoot}/${project.generatedDirectory}`;
|
|
5982
|
+
const manifestPath = `${generated}/${MANIFEST_FILE}`;
|
|
5983
|
+
const resultsPath = `${generated}/${RESULTS_FILE}`;
|
|
5984
|
+
const previousPath = `${generated}/previous-test-manifest.json`;
|
|
5985
|
+
const [manifestSource, resultsSource, previousSource] = await Promise.all([
|
|
5986
|
+
readJson(manifestPath),
|
|
5987
|
+
readJson(resultsPath),
|
|
5988
|
+
readJson(previousPath)
|
|
5989
|
+
]);
|
|
5990
|
+
const manifest = manifestSource === void 0 ? emptyManifest() : parseManifest(manifestSource, `${project.generatedDirectory}/${MANIFEST_FILE}`);
|
|
5991
|
+
const results = resultsSource === void 0 ? [] : parseResults(resultsSource, `${project.generatedDirectory}/${RESULTS_FILE}`);
|
|
5992
|
+
return {
|
|
5993
|
+
manifest,
|
|
5994
|
+
...previousSource === void 0 ? {} : { previousManifest: parseManifest(previousSource, `${project.generatedDirectory}/previous-test-manifest.json`) },
|
|
5995
|
+
results
|
|
5996
|
+
};
|
|
5997
|
+
}
|
|
5998
|
+
async function discoverTestEvidence(project) {
|
|
5999
|
+
return discoverManifest(project);
|
|
6000
|
+
}
|
|
6001
|
+
function testEvidencePaths(projectRoot, generatedDirectory) {
|
|
6002
|
+
const generated = `${projectRoot}/${generatedDirectory}`;
|
|
6003
|
+
return {
|
|
6004
|
+
manifestPath: `${generated}/${MANIFEST_FILE}`,
|
|
6005
|
+
resultsPath: `${generated}/${RESULTS_FILE}`
|
|
6006
|
+
};
|
|
6007
|
+
}
|
|
6008
|
+
|
|
5724
6009
|
// cli/evidence/inventory.ts
|
|
5725
6010
|
async function inventory(project, options) {
|
|
5726
6011
|
const artifact2 = await capture(project, options);
|
|
5727
6012
|
const governed = await requirements(project, options);
|
|
5728
|
-
const discovered = await routes(project, artifact2);
|
|
6013
|
+
const discovered = await routes(project, artifact2, governed.requirements);
|
|
5729
6014
|
return {
|
|
5730
6015
|
projectRootDisplay: project.projectRoot,
|
|
5731
6016
|
requirements: governed.requirements,
|
|
@@ -5750,10 +6035,7 @@ async function inventory(project, options) {
|
|
|
5750
6035
|
async function checkLoader(projectRoot, revision2, _scope) {
|
|
5751
6036
|
const project = await locations({ projectRoot });
|
|
5752
6037
|
const evidence = await inventory(project, { projectRoot, revision: revision2 });
|
|
5753
|
-
const
|
|
5754
|
-
schema: "sleepy-hollow-test-manifest/v1",
|
|
5755
|
-
tests: []
|
|
5756
|
-
};
|
|
6038
|
+
const testEvidence = await readTestEvidence(project);
|
|
5757
6039
|
return {
|
|
5758
6040
|
projectRootDisplay: projectRoot,
|
|
5759
6041
|
requestedScope: { kind: "full" },
|
|
@@ -5765,14 +6047,17 @@ async function checkLoader(projectRoot, revision2, _scope) {
|
|
|
5765
6047
|
criteria: item.criteria,
|
|
5766
6048
|
...item.approval ? { approval: item.approval } : {},
|
|
5767
6049
|
path: item.path,
|
|
5768
|
-
redStateValid: item.
|
|
6050
|
+
redStateValid: item.redStateValid,
|
|
6051
|
+
...item.routePath ? { routePath: item.routePath } : {},
|
|
6052
|
+
...item.methods ? { methods: item.methods } : {}
|
|
5769
6053
|
})),
|
|
5770
6054
|
dependencyGraph: evidence.requirements.map((item) => ({
|
|
5771
6055
|
id: item.id,
|
|
5772
6056
|
dependsOn: item.dependsOn ?? []
|
|
5773
6057
|
})),
|
|
5774
|
-
testManifest,
|
|
5775
|
-
|
|
6058
|
+
testManifest: testEvidence.manifest,
|
|
6059
|
+
previousTestManifest: testEvidence.previousManifest,
|
|
6060
|
+
testResults: testEvidence.results,
|
|
5776
6061
|
routes: evidence.routes.map((route) => ({
|
|
5777
6062
|
requirementId: route.requirementId ?? "",
|
|
5778
6063
|
method: route.method,
|
|
@@ -5802,6 +6087,11 @@ async function checkLoader(projectRoot, revision2, _scope) {
|
|
|
5802
6087
|
async function testLoader(projectRoot) {
|
|
5803
6088
|
const project = await locations({ projectRoot });
|
|
5804
6089
|
const governed = await requirements(project, { projectRoot });
|
|
6090
|
+
const manifest = await discoverTestEvidence(project);
|
|
6091
|
+
const evidencePaths = testEvidencePaths(
|
|
6092
|
+
projectRoot,
|
|
6093
|
+
project.generatedDirectory
|
|
6094
|
+
);
|
|
5805
6095
|
return {
|
|
5806
6096
|
projectRootDisplay: projectRoot,
|
|
5807
6097
|
requirements: governed.requirements.map((item) => ({
|
|
@@ -5817,8 +6107,14 @@ async function testLoader(projectRoot) {
|
|
|
5817
6107
|
dependsOn: item.dependsOn ?? []
|
|
5818
6108
|
})),
|
|
5819
6109
|
routes: [],
|
|
5820
|
-
manifest
|
|
5821
|
-
isolation:
|
|
6110
|
+
manifest,
|
|
6111
|
+
isolation: manifest.tests.map((test) => ({
|
|
6112
|
+
testId: test.id,
|
|
6113
|
+
policy: "isolated"
|
|
6114
|
+
})),
|
|
6115
|
+
manifestPath: evidencePaths.manifestPath,
|
|
6116
|
+
resultsPath: evidencePaths.resultsPath,
|
|
6117
|
+
captureArtifactPath: `${projectRoot}/${project.generatedDirectory}/capture.json`
|
|
5822
6118
|
};
|
|
5823
6119
|
}
|
|
5824
6120
|
|
|
@@ -5851,7 +6147,7 @@ function runCli(args, io, dependencies = {}) {
|
|
|
5851
6147
|
function invokedAsCli(entry = process.argv[1]) {
|
|
5852
6148
|
if (!entry) return false;
|
|
5853
6149
|
try {
|
|
5854
|
-
return import.meta.url ===
|
|
6150
|
+
return import.meta.url === pathToFileURL5(realpathSync(entry)).href;
|
|
5855
6151
|
} catch {
|
|
5856
6152
|
return false;
|
|
5857
6153
|
}
|