@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/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { R as RequirementEvidence, k as RequirementDependency, f as TestManifest, g as TestExecutionResult } from './types-Bet36nZS.js';
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.4";
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-RYKRRAPD.js";
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 pathToFileURL4 } from "url";
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.4";
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.4";
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 relative6 of createdFiles) {
1205
- const target = join2(staging, relative6);
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[relative6], { createNew: true });
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.4",
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.4",
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=verbose",
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(/^(ok|not ok) \d+ - (.*?)(?: # (SKIP).*)?$/i);
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 name = matched[2];
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: byName.get(name) ?? "<runner>",
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 ? normalized : {
4075
- ...normalized,
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.4";
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 approvalStart = governance.indexOf("### Approval");
5231
- if (approvalStart < 0) return void 0;
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
- approvalStart + "### Approval".length
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(root.absolute, root.display, root.serviceId, options)
5599
+ ...await collect(
5600
+ root.absolute,
5601
+ root.display,
5602
+ root.serviceId,
5603
+ "endpoint",
5604
+ options
5605
+ )
5506
5606
  );
5507
5607
  }
5508
- files2.sort((left, right) => left.path.localeCompare(right.path));
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 files2) {
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, "endpoint");
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: false
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
- async function routes(project, artifact2) {
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}${route.source.startsWith("/") ? "" : "/"}${route.source}`,
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 testManifest = {
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.approvalBound
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
- testResults: [],
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: { schema: "sleepy-hollow-test-manifest/v1", tests: [] },
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 === pathToFileURL4(realpathSync(entry)).href;
6150
+ return import.meta.url === pathToFileURL5(realpathSync(entry)).href;
5855
6151
  } catch {
5856
6152
  return false;
5857
6153
  }