@sleepy-hollow/framework 0.3.3 → 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.js CHANGED
@@ -1,28 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  resolveConfiguration
4
- } from "./chunk-4PPSJ2LE.js";
4
+ } from "./chunk-2GNHTFCZ.js";
5
5
  import {
6
+ CRITERION_TEST_DISCOVERY,
7
+ createTestManifest,
6
8
  createTraceabilityReport,
7
9
  selectAffectedTests
8
- } from "./chunk-M4D63YC7.js";
10
+ } from "./chunk-YBEJCNGD.js";
9
11
  import "./chunk-BAKXP7IR.js";
10
12
  import {
11
13
  composeProjectSecurity
12
- } from "./chunk-LZ2HLHDW.js";
14
+ } from "./chunk-AADQFGT4.js";
13
15
  import {
14
16
  normalizeRoutes
15
- } from "./chunk-JRFHLLLF.js";
17
+ } from "./chunk-QY3TSXM6.js";
16
18
  import {
17
19
  discoverRoutes,
18
20
  platform
19
- } from "./chunk-S3Z6CO7J.js";
20
- import "./chunk-LNJDFJGT.js";
21
+ } from "./chunk-LPPASMRR.js";
22
+ import "./chunk-EHBEUJIZ.js";
21
23
  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.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.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 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) {
@@ -1359,6 +1367,7 @@ async function startWorker(options, expectedRoutes) {
1359
1367
  const child = workerCommand(options, "serve").spawn();
1360
1368
  const stdout = readFirstLine(child.stdout);
1361
1369
  const stderr = readBounded(child.stderr);
1370
+ const status = child.status;
1362
1371
  let timeout;
1363
1372
  const timer = new Promise((_, reject) => {
1364
1373
  timeout = setTimeout(
@@ -1387,9 +1396,34 @@ async function startWorker(options, expectedRoutes) {
1387
1396
  );
1388
1397
  }
1389
1398
  let stopped = false;
1399
+ const failure3 = new Promise((_, reject) => {
1400
+ void status.then(async (result) => {
1401
+ if (stopped) return;
1402
+ if (result.success) {
1403
+ reject(new DevCommandError([{
1404
+ code: "SH_DEV_WORKER_EXITED",
1405
+ severity: "error",
1406
+ summary: "The development worker exited after becoming active",
1407
+ correction: "Inspect the worker diagnostics and retry."
1408
+ }]));
1409
+ return;
1410
+ }
1411
+ reject(await workerFailure(
1412
+ await stderr,
1413
+ "The development worker failed after becoming active"
1414
+ ));
1415
+ }, async () => {
1416
+ if (stopped) return;
1417
+ reject(await workerFailure(
1418
+ await stderr,
1419
+ "The development worker failed after becoming active"
1420
+ ));
1421
+ });
1422
+ });
1390
1423
  return {
1391
1424
  url: `http://${options.hostname}:${options.port}/`,
1392
1425
  routeCount: expectedRoutes,
1426
+ failure: failure3,
1393
1427
  async stop() {
1394
1428
  if (stopped) return;
1395
1429
  stopped = true;
@@ -1620,6 +1654,7 @@ function failure2(error, fallback) {
1620
1654
  }
1621
1655
  function changedPaths(projectRoot, paths) {
1622
1656
  const normalized = [];
1657
+ const runtimeDatabaseArtifact = /^\.sleepyhollow\/[^/]+\.(?:sqlite|db)(?:-(?:wal|shm))?$/;
1623
1658
  for (const path of paths) {
1624
1659
  const absolute = isAbsolute(path) ? resolve2(path) : resolve2(projectRoot, path);
1625
1660
  const local = relative(projectRoot, absolute).split(sep).join("/");
@@ -1630,7 +1665,7 @@ function changedPaths(projectRoot, paths) {
1630
1665
  "Watch only project-contained application inputs."
1631
1666
  );
1632
1667
  }
1633
- if (local === ".git" || local.startsWith(".git/") || local === "generated" || local.startsWith("generated/") || local.includes("/node_modules/") || /(?:^|\/)(?:\.DS_Store|.*(?:\.swp|~))$/.test(local)) continue;
1668
+ if (local === ".git" || local.startsWith(".git/") || local === "generated" || local.startsWith("generated/") || local.includes("/node_modules/") || runtimeDatabaseArtifact.test(local) || /(?:^|\/)(?:\.DS_Store|.*(?:\.swp|~))$/.test(local)) continue;
1634
1669
  normalized.push(local);
1635
1670
  }
1636
1671
  return Object.freeze([...new Set(normalized)].sort());
@@ -1645,6 +1680,21 @@ function abortReason(signal) {
1645
1680
  }
1646
1681
  return "cancelled";
1647
1682
  }
1683
+ async function nextChange(iterator, active) {
1684
+ const change = iterator.next();
1685
+ if (!active?.failure) return change;
1686
+ return Promise.race([
1687
+ change,
1688
+ active.failure.then(() => {
1689
+ throw new DevCommandError([{
1690
+ code: "SH_DEV_WORKER_EXITED",
1691
+ severity: "error",
1692
+ summary: "The active development worker exited unexpectedly",
1693
+ correction: "Inspect the worker diagnostics and retry."
1694
+ }]);
1695
+ })
1696
+ ]);
1697
+ }
1648
1698
  async function runDevCommand(args, io, suppliedDependencies) {
1649
1699
  const parsed = parse2(args);
1650
1700
  if ("code" in parsed) {
@@ -1766,7 +1816,11 @@ ${parsed.correction}`
1766
1816
  return 1;
1767
1817
  }
1768
1818
  if (signal.aborted) await closeWatcher();
1769
- for await (const paths of watcher) {
1819
+ const iterator = watcher[Symbol.asyncIterator]();
1820
+ while (true) {
1821
+ const next = await nextChange(iterator, active);
1822
+ if (next.done) break;
1823
+ const paths = next.value;
1770
1824
  if (signal.aborted) break;
1771
1825
  const changes = changedPaths(projectRoot, paths);
1772
1826
  if (!Array.isArray(changes)) {
@@ -1919,14 +1973,26 @@ function safeDiagnostic(error, intent) {
1919
1973
  ...typeof item.key === "string" ? { configuration: [item.key] } : {}
1920
1974
  }));
1921
1975
  }
1922
- if (intent === "serve" && (error instanceof platform.errors.AddrInUse || error instanceof platform.errors.PermissionDenied)) {
1976
+ const nodeError = error;
1977
+ const nodeCode = typeof nodeError?.code === "string" ? nodeError.code : void 0;
1978
+ const isListenFailure = error instanceof platform.errors.AddrInUse || error instanceof platform.errors.PermissionDenied || nodeError?.syscall === "listen" || nodeCode === "EADDRINUSE" || nodeCode === "EACCES" || nodeCode === "EPERM";
1979
+ if (intent === "serve" && isListenFailure) {
1923
1980
  return [{
1924
1981
  code: "SH_DEV_BIND_FAILED",
1925
1982
  severity: "error",
1926
- summary: "The loopback listener could not bind",
1983
+ summary: nodeCode ? `The loopback listener failed with ${nodeCode}` : "The loopback listener could not bind",
1927
1984
  correction: "Choose an available port and confirm local network access."
1928
1985
  }];
1929
1986
  }
1987
+ if (intent === "serve") {
1988
+ const cause = error instanceof Error ? `${error.name}: ${error.message}`.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/(?:Bearer|token|password|secret|authorization)[=: ]+\S+/gi, "$1=[redacted]").replace(/(?:[A-Za-z]:)?\/[^\s]+/g, "[path]").slice(0, 180) : void 0;
1989
+ return [{
1990
+ code: "SH_DEV_SERVE_FAILED",
1991
+ severity: "error",
1992
+ summary: cause ? `The development server failed: ${cause}` : "The development server failed",
1993
+ correction: "Inspect the bounded server diagnostic and retry after repairing the host boundary."
1994
+ }];
1995
+ }
1930
1996
  return [{
1931
1997
  code: "SH_DEV_PROJECT_INVALID",
1932
1998
  severity: "error",
@@ -1998,20 +2064,20 @@ async function runDevWorker(args) {
1998
2064
  const [intent, projectRoot, hostname, rawPort] = args;
1999
2065
  const port = Number(rawPort);
2000
2066
  if (hostname !== "127.0.0.1" || !Number.isInteger(port) || port < 1 || port > 65535) return 2;
2067
+ let server;
2001
2068
  try {
2002
2069
  const { runtime: runtime2, routeCount } = await loadRuntime(projectRoot);
2003
2070
  if (intent === "validate") {
2004
2071
  console.log(JSON.stringify({ ready: true, routeCount }));
2005
2072
  return 0;
2006
2073
  }
2007
- const server = platform.serve({ hostname, port }, (request) => runtime2.fetch(request));
2074
+ server = platform.serve({ hostname, port }, (request) => runtime2.fetch(request));
2075
+ await server.ready;
2008
2076
  console.log(JSON.stringify({ ready: true, routeCount }));
2009
- await new Promise((resolve5, reject) => {
2010
- server.once("close", resolve5);
2011
- server.once("error", reject);
2012
- });
2077
+ await server.finished;
2013
2078
  return 0;
2014
2079
  } catch (error) {
2080
+ await server?.shutdown().catch(() => void 0);
2015
2081
  console.error(
2016
2082
  JSON.stringify({
2017
2083
  ready: false,
@@ -2911,7 +2977,7 @@ function renderArtifacts(inventory2) {
2911
2977
  ];
2912
2978
  const manifestContent = canonicalJson({
2913
2979
  schema: "sleepy-hollow-generated-manifest/v1",
2914
- generatorVersion: "0.3.3",
2980
+ generatorVersion: "0.3.5",
2915
2981
  serviceId: normalized.serviceId,
2916
2982
  inputDigest: digest2(input),
2917
2983
  artifacts: Object.fromEntries(
@@ -3185,7 +3251,7 @@ function inventoryFromRoutes(routes2, options) {
3185
3251
  return {
3186
3252
  serviceId: options.serviceId,
3187
3253
  title: options.title ?? options.serviceId,
3188
- version: options.version ?? "0.3.3",
3254
+ version: options.version ?? "0.3.5",
3189
3255
  ...options.description ? { description: options.description } : {},
3190
3256
  operations,
3191
3257
  securitySchemes: options.securitySchemes ?? {}
@@ -3416,7 +3482,7 @@ function invocation(plan2, options) {
3416
3482
  const args = [
3417
3483
  "./node_modules/vitest/vitest.mjs",
3418
3484
  "run",
3419
- "--reporter=verbose",
3485
+ "--reporter=tap-flat",
3420
3486
  ...plan2.filter ? ["--testNamePattern", plan2.filter] : [],
3421
3487
  ...files2
3422
3488
  ];
@@ -3464,11 +3530,16 @@ function parseTap(source, inventory2, failureEvidence) {
3464
3530
  );
3465
3531
  const events = [];
3466
3532
  for (const line of lines) {
3467
- 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
+ );
3468
3536
  if (!matched) continue;
3469
- 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;
3470
3541
  events.push({
3471
- file: byName.get(name) ?? "<runner>",
3542
+ file,
3472
3543
  name,
3473
3544
  status: matched[3]?.toUpperCase() === "SKIP" ? "skipped" : matched[1] === "ok" ? "passed" : "failed",
3474
3545
  ...matched[1] === "not ok" && failureEvidence ? { evidence: failureEvidence } : {}
@@ -3829,6 +3900,32 @@ function renderJson(result) {
3829
3900
  }
3830
3901
 
3831
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
+ }
3832
3929
  function parse3(args) {
3833
3930
  let json2 = false;
3834
3931
  let scope = { kind: "full" };
@@ -3954,7 +4051,7 @@ async function execute(args, io, load, runner) {
3954
4051
  } catch {
3955
4052
  return emit(loadFailure(parsed.scope), parsed.json, io);
3956
4053
  }
3957
- const capturePath = `${io.cwd}/generated/capture.json`;
4054
+ const capturePath = inventory2.captureArtifactPath ?? `${io.cwd}/generated/capture.json`;
3958
4055
  inventory2 = { ...inventory2, captureArtifactPath: capturePath };
3959
4056
  const testPlan = plan(inventory2, parsed.scope);
3960
4057
  if (testPlan.diagnostics.some((item) => item.severity === "error")) {
@@ -4006,6 +4103,26 @@ async function execute(args, io, load, runner) {
4006
4103
  };
4007
4104
  }
4008
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
+ }
4009
4126
  let persisted = true;
4010
4127
  try {
4011
4128
  await platform.stat(capturePath);
@@ -4013,8 +4130,8 @@ async function execute(args, io, load, runner) {
4013
4130
  persisted = false;
4014
4131
  }
4015
4132
  return emit(
4016
- persisted ? normalized : {
4017
- ...normalized,
4133
+ persisted ? persistedResult : {
4134
+ ...persistedResult,
4018
4135
  diagnostics: [
4019
4136
  ...normalized.diagnostics,
4020
4137
  {
@@ -4039,7 +4156,7 @@ var CLI_COMMANDS = [
4039
4156
  "generate",
4040
4157
  "deploy"
4041
4158
  ];
4042
- var CLI_VERSION = "0.3.3";
4159
+ var CLI_VERSION = "0.3.5";
4043
4160
  var commandMetadata = {
4044
4161
  create: {
4045
4162
  description: "Create one deterministic Sleepy Hollow project.",
@@ -5169,10 +5286,11 @@ function digestOf(source, path) {
5169
5286
  }
5170
5287
  function approvalOf(source, digest3, criteria) {
5171
5288
  const governance = source.slice(source.indexOf("## Governance record"));
5172
- const approvalStart = governance.indexOf("### Approval");
5173
- 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;
5174
5292
  const approvalRemainder = governance.slice(
5175
- approvalStart + "### Approval".length
5293
+ latest.index + latest[0].length
5176
5294
  );
5177
5295
  const next = approvalRemainder.search(/^### /m);
5178
5296
  const approval = next < 0 ? approvalRemainder : approvalRemainder.slice(0, next);
@@ -5181,7 +5299,7 @@ function approvalOf(source, digest3, criteria) {
5181
5299
  const approvedAt = /- Approved at:\s*(.+?)\.?(?:\r?\n|$)/.exec(approval)?.[1]?.replace(/\.$/, "");
5182
5300
  const criterionText = /- Approved criteria:\s*(.+?)\.?(?:\r?\n|$)/.exec(approval)?.[1] ?? "";
5183
5301
  const recordedDigest = /sha256:([a-f0-9]{64})/.exec(approval)?.[1];
5184
- 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(/\.$/, "");
5185
5303
  const approvedCriteria = criteria.filter(
5186
5304
  (item) => criterionText.includes(item.id)
5187
5305
  ).map((item) => item.id);
@@ -5197,6 +5315,18 @@ function approvalOf(source, digest3, criteria) {
5197
5315
  valid
5198
5316
  };
5199
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
+ }
5200
5330
  function parseRequirementDocument(source, path, kind) {
5201
5331
  const { metadata, body, bodyOffset } = frontmatterOf(source, path);
5202
5332
  const diagnostics = [];
@@ -5377,7 +5507,8 @@ function parseRequirementDocument(source, path, kind) {
5377
5507
  criteria: criterionResult.criteria,
5378
5508
  sections,
5379
5509
  governedContentDigest,
5380
- approval
5510
+ approval,
5511
+ redStateValid: redStateValid(source)
5381
5512
  };
5382
5513
  }
5383
5514
 
@@ -5390,7 +5521,7 @@ function parseRequirement(source, path, kind) {
5390
5521
  function issue2(code, path, summary, correction, line) {
5391
5522
  return { code, path, summary, correction, ...line ? { line } : {} };
5392
5523
  }
5393
- async function collect(absoluteRoot, displayRoot, serviceId2, options) {
5524
+ async function collect(absoluteRoot, displayRoot, serviceId2, kind, options) {
5394
5525
  const list = options.listDirectory ?? (async (path) => {
5395
5526
  const entries = [];
5396
5527
  for await (const entry of platform.readDir(path)) {
@@ -5422,6 +5553,7 @@ async function collect(absoluteRoot, displayRoot, serviceId2, options) {
5422
5553
  path: nextDisplay,
5423
5554
  absolutePath: nextAbsolute,
5424
5555
  legacy: entry.name === "requirements.md",
5556
+ kind,
5425
5557
  ...serviceId2 ? { serviceId: serviceId2 } : {}
5426
5558
  });
5427
5559
  }
@@ -5442,16 +5574,45 @@ async function requirements(project, options) {
5442
5574
  serviceId: void 0
5443
5575
  }];
5444
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
+ }
5445
5597
  for (const root of roots) {
5446
5598
  files2.push(
5447
- ...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
+ )
5448
5606
  );
5449
5607
  }
5450
- 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));
5451
5612
  const diagnostics = [];
5452
5613
  const loaded = [];
5453
5614
  const seen = /* @__PURE__ */ new Map();
5454
- for (const file of files2) {
5615
+ for (const file of uniqueFiles) {
5455
5616
  if (file.legacy) {
5456
5617
  diagnostics.push(issue2(
5457
5618
  "SH_EVIDENCE_REQUIREMENT_LEGACY_FILENAME",
@@ -5475,7 +5636,7 @@ async function requirements(project, options) {
5475
5636
  }
5476
5637
  let parsed;
5477
5638
  try {
5478
- parsed = parseRequirement(source, file.path, "endpoint");
5639
+ parsed = parseRequirement(source, file.path, file.kind);
5479
5640
  } catch (error) {
5480
5641
  if (error instanceof PlanningError) {
5481
5642
  for (const item of error.diagnostics) {
@@ -5527,7 +5688,14 @@ async function requirements(project, options) {
5527
5688
  } : {},
5528
5689
  path: file.path,
5529
5690
  ...file.serviceId ? { serviceId: file.serviceId } : {},
5530
- 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
+ } : {}
5531
5699
  });
5532
5700
  }
5533
5701
  if (diagnostics.length > 0) throw new EvidenceError(diagnostics);
@@ -5544,12 +5712,15 @@ async function requirements(project, options) {
5544
5712
  criteria: item.criteria,
5545
5713
  ...item.approval ? { approval: item.approval } : {},
5546
5714
  path: item.path,
5547
- redStateValid: false
5715
+ redStateValid: item.redStateValid,
5716
+ ...item.routePath ? { routePath: item.routePath } : {},
5717
+ ...item.methods ? { methods: item.methods } : {}
5548
5718
  }))
5549
5719
  };
5550
5720
  }
5551
5721
 
5552
5722
  // cli/evidence/behavior.ts
5723
+ import { relative as relative6, sep as sep7 } from "path";
5553
5724
  var CAPTURE_FILE = "capture.json";
5554
5725
  async function capture(project, options) {
5555
5726
  const path = `${project.projectRoot}/${project.generatedDirectory}/${CAPTURE_FILE}`;
@@ -5612,7 +5783,16 @@ function authenticationOf(security2) {
5612
5783
  if (typeof declared !== "object" || declared === null) return "none";
5613
5784
  return declared.mode === "required" ? "required" : "none";
5614
5785
  }
5615
- 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 = []) {
5616
5796
  const roots = project.services.length > 0 ? project.services.map((service) => ({
5617
5797
  absolute: `${project.projectRoot}/${service.apiRoot}`,
5618
5798
  display: service.apiRoot
@@ -5645,10 +5825,12 @@ async function routes(project, artifact2) {
5645
5825
  }
5646
5826
  statuses2.add(request.responseStatus);
5647
5827
  }
5828
+ const source = route.source.startsWith("/") ? relative6(root.absolute, route.source).split(sep7).join("/") : route.source;
5648
5829
  found.push({
5830
+ requirementId: ownerOf(route, requirements2),
5649
5831
  method: route.method,
5650
5832
  path: route.path,
5651
- source: `${root.display}${route.source.startsWith("/") ? "" : "/"}${route.source}`,
5833
+ source: `${root.display}/${source}`,
5652
5834
  requestSchemaLocations: declaredLocations(route.operation.schemas),
5653
5835
  requiredRequestLocations: [...readLocations].sort(),
5654
5836
  responseSchemaStatuses: declaredStatuses(route.operation.schemas),
@@ -5663,11 +5845,172 @@ async function routes(project, artifact2) {
5663
5845
  );
5664
5846
  }
5665
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
+
5666
6009
  // cli/evidence/inventory.ts
5667
6010
  async function inventory(project, options) {
5668
6011
  const artifact2 = await capture(project, options);
5669
6012
  const governed = await requirements(project, options);
5670
- const discovered = await routes(project, artifact2);
6013
+ const discovered = await routes(project, artifact2, governed.requirements);
5671
6014
  return {
5672
6015
  projectRootDisplay: project.projectRoot,
5673
6016
  requirements: governed.requirements,
@@ -5692,10 +6035,7 @@ async function inventory(project, options) {
5692
6035
  async function checkLoader(projectRoot, revision2, _scope) {
5693
6036
  const project = await locations({ projectRoot });
5694
6037
  const evidence = await inventory(project, { projectRoot, revision: revision2 });
5695
- const testManifest = {
5696
- schema: "sleepy-hollow-test-manifest/v1",
5697
- tests: []
5698
- };
6038
+ const testEvidence = await readTestEvidence(project);
5699
6039
  return {
5700
6040
  projectRootDisplay: projectRoot,
5701
6041
  requestedScope: { kind: "full" },
@@ -5707,14 +6047,17 @@ async function checkLoader(projectRoot, revision2, _scope) {
5707
6047
  criteria: item.criteria,
5708
6048
  ...item.approval ? { approval: item.approval } : {},
5709
6049
  path: item.path,
5710
- redStateValid: item.approvalBound
6050
+ redStateValid: item.redStateValid,
6051
+ ...item.routePath ? { routePath: item.routePath } : {},
6052
+ ...item.methods ? { methods: item.methods } : {}
5711
6053
  })),
5712
6054
  dependencyGraph: evidence.requirements.map((item) => ({
5713
6055
  id: item.id,
5714
6056
  dependsOn: item.dependsOn ?? []
5715
6057
  })),
5716
- testManifest,
5717
- testResults: [],
6058
+ testManifest: testEvidence.manifest,
6059
+ previousTestManifest: testEvidence.previousManifest,
6060
+ testResults: testEvidence.results,
5718
6061
  routes: evidence.routes.map((route) => ({
5719
6062
  requirementId: route.requirementId ?? "",
5720
6063
  method: route.method,
@@ -5744,6 +6087,11 @@ async function checkLoader(projectRoot, revision2, _scope) {
5744
6087
  async function testLoader(projectRoot) {
5745
6088
  const project = await locations({ projectRoot });
5746
6089
  const governed = await requirements(project, { projectRoot });
6090
+ const manifest = await discoverTestEvidence(project);
6091
+ const evidencePaths = testEvidencePaths(
6092
+ projectRoot,
6093
+ project.generatedDirectory
6094
+ );
5747
6095
  return {
5748
6096
  projectRootDisplay: projectRoot,
5749
6097
  requirements: governed.requirements.map((item) => ({
@@ -5759,8 +6107,14 @@ async function testLoader(projectRoot) {
5759
6107
  dependsOn: item.dependsOn ?? []
5760
6108
  })),
5761
6109
  routes: [],
5762
- manifest: { schema: "sleepy-hollow-test-manifest/v1", tests: [] },
5763
- 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`
5764
6118
  };
5765
6119
  }
5766
6120
 
@@ -5793,7 +6147,7 @@ function runCli(args, io, dependencies = {}) {
5793
6147
  function invokedAsCli(entry = process.argv[1]) {
5794
6148
  if (!entry) return false;
5795
6149
  try {
5796
- return import.meta.url === pathToFileURL4(realpathSync(entry)).href;
6150
+ return import.meta.url === pathToFileURL5(realpathSync(entry)).href;
5797
6151
  } catch {
5798
6152
  return false;
5799
6153
  }