@rehearsal-db/core 0.1.0-rc.4 → 0.1.0-rc.8

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.
Files changed (180) hide show
  1. package/BENCHMARKS.md +18 -0
  2. package/CHANGELOG.md +106 -1
  3. package/COMPATIBILITY.md +1 -1
  4. package/README.md +10 -0
  5. package/SECURITY.md +4 -3
  6. package/dist/package.json +10 -3
  7. package/dist/scripts/runtime/manage_database.mjs +12 -2
  8. package/dist/src/application/activity_contract.d.mts +28 -0
  9. package/dist/src/application/activity_contract.mjs +84 -0
  10. package/dist/src/application/activity_monitor.d.mts +20 -0
  11. package/dist/src/application/activity_monitor.mjs +191 -0
  12. package/dist/src/application/port_preflight.d.mts +12 -0
  13. package/dist/src/application/port_preflight.mjs +71 -0
  14. package/dist/src/application/process_group.d.mts +13 -0
  15. package/dist/src/application/process_group.mjs +55 -0
  16. package/dist/src/application/session.d.mts +11 -3
  17. package/dist/src/application/session.mjs +62 -20
  18. package/dist/src/baseline/artifact.d.mts +3 -1
  19. package/dist/src/baseline/artifact.mjs +41 -2
  20. package/dist/src/cli/application_command_handlers.mjs +1 -1
  21. package/dist/src/cli/arguments.d.mts +4 -0
  22. package/dist/src/cli/arguments.mjs +20 -0
  23. package/dist/src/cli/baseline_shape_rendering.d.mts +3 -0
  24. package/dist/src/cli/baseline_shape_rendering.mjs +32 -0
  25. package/dist/src/cli/basic_command_handlers.mjs +7 -1
  26. package/dist/src/cli/cli_commands.mjs +2 -0
  27. package/dist/src/cli/editor_command_handlers.d.mts +2 -0
  28. package/dist/src/cli/editor_command_handlers.mjs +60 -0
  29. package/dist/src/cli/guided.mjs +150 -135
  30. package/dist/src/cli/guided_updates.d.mts +14 -0
  31. package/dist/src/cli/guided_updates.mjs +148 -0
  32. package/dist/src/cli/identity_command_handlers.mjs +13 -9
  33. package/dist/src/cli/inspection_command_handlers.mjs +46 -0
  34. package/dist/src/cli/project_commands.d.mts +48 -0
  35. package/dist/src/cli/project_commands.mjs +11 -3
  36. package/dist/src/cli/rehearsal.mjs +59 -48
  37. package/dist/src/cli/renderers.d.mts +9 -0
  38. package/dist/src/cli/renderers.mjs +18 -0
  39. package/dist/src/cli/runtime_action_handlers.mjs +11 -7
  40. package/dist/src/cli/runtime_commands.d.mts +15 -204
  41. package/dist/src/cli/runtime_commands.mjs +143 -175
  42. package/dist/src/cli/runtime_failure_summary.d.mts +1 -0
  43. package/dist/src/cli/runtime_failure_summary.mjs +53 -0
  44. package/dist/src/cli/runtime_planning.d.mts +278 -0
  45. package/dist/src/cli/runtime_planning.mjs +130 -0
  46. package/dist/src/cli/runtime_stack_execution.d.mts +6 -0
  47. package/dist/src/cli/runtime_stack_execution.mjs +31 -0
  48. package/dist/src/cli/source_command_handlers.mjs +7 -0
  49. package/dist/src/cli/source_commands.d.mts +81 -0
  50. package/dist/src/cli/source_commands.mjs +38 -7
  51. package/dist/src/cli/update_advisory.d.mts +29 -0
  52. package/dist/src/cli/update_advisory.mjs +415 -0
  53. package/dist/src/index.d.mts +2 -1
  54. package/dist/src/index.mjs +0 -1
  55. package/dist/src/project/configuration.d.mts +1 -1
  56. package/dist/src/project/configuration_activity.d.mts +11 -0
  57. package/dist/src/project/configuration_activity.mjs +31 -0
  58. package/dist/src/project/configuration_application_urls.d.mts +10 -0
  59. package/dist/src/project/configuration_application_urls.mjs +18 -0
  60. package/dist/src/project/configuration_contract.d.mts +47 -0
  61. package/dist/src/project/configuration_contract.mjs +14 -1
  62. package/dist/src/project/configuration_normalization.d.mts +23 -0
  63. package/dist/src/project/configuration_normalization.mjs +60 -3
  64. package/dist/src/project/configuration_profiles.d.mts +12 -0
  65. package/dist/src/project/configuration_profiles.mjs +33 -0
  66. package/dist/src/project/configuration_rendering.mjs +45 -0
  67. package/dist/src/project/configuration_resolution.d.mts +24 -0
  68. package/dist/src/project/configuration_resolution.mjs +3 -0
  69. package/dist/src/project/configuration_resource_policy.d.mts +15 -0
  70. package/dist/src/project/configuration_resource_policy.mjs +65 -0
  71. package/dist/src/project/configuration_review.d.mts +46 -0
  72. package/dist/src/project/configuration_review.mjs +228 -0
  73. package/dist/src/project/configuration_schema.d.mts +2 -0
  74. package/dist/src/project/configuration_schema.mjs +28 -2
  75. package/dist/src/project/configuration_validation.d.mts +3 -0
  76. package/dist/src/project/configuration_validation.mjs +16 -3
  77. package/dist/src/runtime/cleanup.d.mts +10 -13
  78. package/dist/src/runtime/cleanup.mjs +64 -77
  79. package/dist/src/runtime/configuration_binding.d.mts +6 -0
  80. package/dist/src/runtime/configuration_binding.mjs +10 -0
  81. package/dist/src/runtime/container_engine_ownership.d.mts +46 -0
  82. package/dist/src/runtime/container_engine_ownership.mjs +240 -0
  83. package/dist/src/runtime/environment_classification.d.mts +45 -0
  84. package/dist/src/runtime/environment_classification.mjs +83 -0
  85. package/dist/src/runtime/lifecycle_engine.d.mts +2 -1
  86. package/dist/src/runtime/lifecycle_engine.mjs +7 -2
  87. package/dist/src/runtime/local_docker_context.d.mts +27 -0
  88. package/dist/src/runtime/local_docker_context.mjs +92 -0
  89. package/dist/src/runtime/migration_plan.d.mts +52 -0
  90. package/dist/src/runtime/migration_plan.mjs +51 -0
  91. package/dist/src/runtime/plan.d.mts +129 -0
  92. package/dist/src/runtime/plan.mjs +56 -7
  93. package/dist/src/runtime/project_review.d.mts +90 -0
  94. package/dist/src/runtime/project_review.mjs +200 -0
  95. package/dist/src/runtime/resource_catalog.d.mts +80 -0
  96. package/dist/src/runtime/resource_catalog.mjs +170 -0
  97. package/dist/src/runtime/resource_images.d.mts +19 -0
  98. package/dist/src/runtime/resource_images.mjs +62 -0
  99. package/dist/src/runtime/resource_inventory.d.mts +93 -0
  100. package/dist/src/runtime/resource_inventory.mjs +509 -0
  101. package/dist/src/runtime/resource_policy.d.mts +38 -0
  102. package/dist/src/runtime/resource_policy.mjs +143 -0
  103. package/dist/src/runtime/resource_provenance.d.mts +31 -0
  104. package/dist/src/runtime/resource_provenance.mjs +143 -0
  105. package/dist/src/runtime/runtime_manager_guards.d.mts +11 -0
  106. package/dist/src/runtime/runtime_manager_guards.mjs +43 -0
  107. package/dist/src/runtime/runtime_startup_ownership.d.mts +36 -0
  108. package/dist/src/runtime/runtime_startup_ownership.mjs +221 -0
  109. package/dist/src/runtime/stale_environment_plan.d.mts +67 -0
  110. package/dist/src/runtime/stale_environment_plan.mjs +174 -0
  111. package/dist/src/runtime/stale_environment_stop.d.mts +12 -0
  112. package/dist/src/runtime/stale_environment_stop.mjs +98 -0
  113. package/dist/src/runtime/topology.d.mts +2 -1
  114. package/dist/src/runtime/topology.mjs +17 -3
  115. package/dist/src/runtime/topology_application_ports.d.mts +6 -0
  116. package/dist/src/runtime/topology_application_ports.mjs +38 -0
  117. package/dist/src/shared/cancellation.mjs +7 -1
  118. package/dist/src/shared/diagnostics.mjs +4 -0
  119. package/dist/src/shared/operation_guard.d.mts +6 -0
  120. package/dist/src/shared/operation_guard.mjs +80 -14
  121. package/dist/src/shared/operation_progress.d.mts +13 -0
  122. package/dist/src/shared/operation_progress.mjs +163 -0
  123. package/dist/src/shared/operation_progress_contract.d.mts +46 -0
  124. package/dist/src/shared/operation_progress_contract.mjs +135 -0
  125. package/dist/src/shared/operation_progress_storage.d.mts +24 -0
  126. package/dist/src/shared/operation_progress_storage.mjs +149 -0
  127. package/dist/src/shared/owned_process.mjs +9 -0
  128. package/dist/src/source/baseline.d.mts +3 -1
  129. package/dist/src/source/baseline.mjs +99 -9
  130. package/dist/src/source/shape.d.mts +8 -0
  131. package/dist/src/source/shape.mjs +6 -0
  132. package/dist/src/source/shape_contract.d.mts +121 -0
  133. package/dist/src/source/shape_contract.mjs +2 -0
  134. package/dist/src/source/shape_evidence.d.mts +27 -0
  135. package/dist/src/source/shape_evidence.mjs +504 -0
  136. package/dist/src/source/shape_files.d.mts +9 -0
  137. package/dist/src/source/shape_files.mjs +30 -0
  138. package/dist/src/source/shape_json.d.mts +7 -0
  139. package/dist/src/source/shape_json.mjs +75 -0
  140. package/dist/src/source/shape_keys.d.mts +16 -0
  141. package/dist/src/source/shape_keys.mjs +84 -0
  142. package/dist/src/source/shape_policy.d.mts +4 -0
  143. package/dist/src/source/shape_policy.mjs +361 -0
  144. package/dist/src/source/shape_preparation.d.mts +20 -0
  145. package/dist/src/source/shape_preparation.mjs +94 -0
  146. package/dist/src/source/shape_profiles.d.mts +16 -0
  147. package/dist/src/source/shape_profiles.mjs +234 -0
  148. package/dist/src/source/shape_sampler.d.mts +14 -0
  149. package/dist/src/source/shape_sampler.mjs +175 -0
  150. package/dist/src/targets/docker_resource_ownership.d.mts +11 -0
  151. package/dist/src/targets/docker_resource_ownership.mjs +28 -0
  152. package/dist/src/targets/postgresql.mjs +31 -35
  153. package/dist/src/targets/postgresql_process.d.mts +10 -1
  154. package/dist/src/targets/postgresql_process.mjs +37 -4
  155. package/dist/src/targets/supabase.d.mts +2 -7
  156. package/dist/src/targets/supabase.mjs +14 -13
  157. package/dist/src/targets/supabase_environment.d.mts +31 -3
  158. package/dist/src/targets/supabase_environment.mjs +205 -12
  159. package/dist/src/targets/supabase_process.d.mts +3 -1
  160. package/dist/src/targets/supabase_process.mjs +7 -4
  161. package/dist/src/targets/supabase_service_requirements.d.mts +6 -0
  162. package/dist/src/targets/supabase_service_requirements.mjs +42 -0
  163. package/dist/src/targets/supabase_start_failure.d.mts +6 -0
  164. package/dist/src/targets/supabase_start_failure.mjs +65 -0
  165. package/docs/architecture.md +13 -0
  166. package/docs/baselines.md +18 -0
  167. package/docs/commands.md +76 -14
  168. package/docs/configuration.md +252 -2
  169. package/docs/getting-started.md +20 -0
  170. package/docs/production-source.md +156 -0
  171. package/docs/releasing.md +116 -15
  172. package/docs/roadmap.md +6 -0
  173. package/docs/runtime-lifecycle.md +247 -8
  174. package/docs/security-model.md +38 -1
  175. package/docs/stability.md +4 -0
  176. package/docs/troubleshooting.md +29 -0
  177. package/docs/tutorial.md +19 -10
  178. package/docs/typescript.md +90 -7
  179. package/docs/vscode.md +54 -17
  180. package/package.json +10 -3
package/BENCHMARKS.md CHANGED
@@ -55,6 +55,24 @@ the same proof remains required in GitHub Actions before public release.
55
55
 
56
56
  ## Regression policy
57
57
 
58
+ The installed PostgreSQL scenario also demonstrates a data-specific failure:
59
+ the exact same `SET NOT NULL` migration succeeds on an empty table and fails on a
60
+ synthetic table containing a known null. It then reviews a changed candidate that
61
+ backfills the synthetic value, verifies the corrected constraint through the
62
+ application's HTTP proof, and checks immutable baselines and resource cleanup.
63
+ This is a correctness control, not a timing benchmark or a claim about production
64
+ traffic. The synthetic backfill is not a recommendation for real business data.
65
+
66
+ Maintainers can repeat it with one exact artifact:
67
+
68
+ ```bash
69
+ REHEARSAL_CANDIDATE_ARTIFACT=/absolute/path/to/candidate.tgz npm run test:fixture:postgresql
70
+ ```
71
+
72
+ The result records the artifact and candidate hashes, empty/populated outcomes,
73
+ baseline preservation and cleanup. Do not attribute a candidate's result to a
74
+ different registry artifact merely because both have the same version number.
75
+
58
76
  Record a new equivalent sample before a beta release or after changing baseline format,
59
77
  streaming, restore, migration application, verification, or local-runtime lifecycle.
60
78
  Investigate a repeatable increase greater than 25% in a phase. Do not weaken checks to
package/CHANGELOG.md CHANGED
@@ -3,7 +3,112 @@
3
3
  All notable Rehearsal package changes will be documented here. The format follows Keep
4
4
  a Changelog, and versions will follow Semantic Versioning after the package exists.
5
5
 
6
- ## Unreleased
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0-rc.8] - Unreleased candidate
9
+
10
+ ### Added
11
+
12
+ - Optional interactive update reminders, with explicit network consent, daily
13
+ opening-session checks, defer/skip/disable choices and exact-version guidance.
14
+ Machine commands remain network-free, custom installations stay manual-only,
15
+ and no update installs automatically.
16
+
17
+ ### Fixed
18
+
19
+ - Invalid positive-integer configuration settings and rejected configuration proposals
20
+ report configuration failures rather than baseline or internal failures. Review
21
+ remains non-writing, and unexpected faults retain their original failure category.
22
+
23
+ ### Documentation
24
+
25
+ - Separate released VSIX installation, dashboard access, and updates from contributor
26
+ setup and separately approved commercial launch requirements.
27
+ - Clarify that configured database and Storage data remain persistent and that no
28
+ separate temporary Docker-helper ownership or cleanup capability exists today.
29
+ - Explain profile startup and resource measurements without equating cached-image
30
+ container checks with a cold VM or whole-machine memory benchmark.
31
+
32
+ ## [0.1.0-rc.7] - 2026-10-15
33
+
34
+ ### Fixed
35
+
36
+ - Release approval checks accept GitHub form line endings without changing the exact
37
+ owner, environment, operation, run, attempt, source or checksum bindings. Other text
38
+ changes and stale or ambiguous approvals remain refused.
39
+
40
+ rc.6's tag and failed GitHub publication history are preserved; it was not published
41
+ to npm. rc.7 does not add or change database, application or environment behavior.
42
+
43
+ ### Documentation
44
+
45
+ - The shared GitHub/npm README visibly links to both the source repository and npm
46
+ package page.
47
+
48
+ ## [0.1.0-rc.6] - 2026-10-15
49
+
50
+ ### Fixed
51
+
52
+ - Release ancestry verification no longer requests a paginated comparison when the
53
+ immutable source tag already points to the exact verified main commit. Older source
54
+ commits still require bounded, unpaged ancestry evidence; incomplete responses remain
55
+ refused. Human approval, provenance and immutable-tag protections are unchanged.
56
+
57
+ rc.5's GitHub candidate and tag are retained, but its npm publication stopped at the
58
+ read-only preflight. rc.6 carries the same product features and a new tested artifact;
59
+ no runtime or environment behavior was added during this publisher correction.
60
+
61
+ ## [0.1.0-rc.5] - 2026-10-15
62
+
63
+ ### Added
64
+
65
+ - Project-owned resource inventory, exact long-running-environment stop review, and
66
+ individually selected unused-image cleanup. Volumes remain tied to reviewed runtime
67
+ discard; other projects and the shared engine are protected.
68
+ - Friendly environment labels, protection, named display profiles, resource quotas and
69
+ review schedules, with validated configuration metadata and fingerprint-bound proposals.
70
+ - Ordered migration review, baseline/privacy/authentication summaries, and bounded
71
+ operation stages and recent receipts through public JSON contracts.
72
+ - Optional target-local resource provenance reports recorded baseline and migration
73
+ digests, restore-writer version, and configured ports without claiming fresh
74
+ verification, listening services or activity. Legacy receipts remain explicitly unknown.
75
+ - Environment classification separately reports ownership-proven creation, recorded
76
+ format/provider compatibility, and age-based review due status. Missing verification
77
+ does not mean a database was never created; differing writer versions do not imply
78
+ incompatible data. Configured sandbox volumes remain persistent after workload stop.
79
+ - Opt-in, bounded graph sampling and measured distribution goals, with immutable
80
+ post-sanitization fidelity evidence and required-goal refusal before activation.
81
+ - Lightweight Supabase service profiles and explicit required-service checks.
82
+ - Opt-in foreground sandbox idle shutdown using an explicit application-owned activity
83
+ contract. Unknown or busy activity preserves workloads; no background daemon is added.
84
+ - A disposable PostgreSQL regression demo showing an empty-database migration pass,
85
+ populated-data refusal, and verified recovery with a reviewed backfill.
86
+
87
+ ### Fixed
88
+
89
+ - Partial startup and interrupted work clean up only workloads proved owned by the
90
+ operation, preserving existing running environments, data and shared engines.
91
+ - Opted-in owned-engine shutdown checks for newly appeared workloads immediately before
92
+ stopping and preserves a replacement ownership receipt from a later operation.
93
+ - Application startup refuses occupied local ports without replacing another process.
94
+ - Application and profile ports are checked across the complete configured topology;
95
+ configuration proposals can explicitly bind a dependent edit to its parent graph.
96
+ - Application teardown retires its owned process group even after the launcher exits.
97
+ - Supabase startup diagnostics preserve safe service/health categories while withholding
98
+ logs, credentials, URLs and private paths. Unknown causes stay unknown.
99
+ - Public JSON retains strictly false privacy attestations without exposing credentials;
100
+ normalized shape policies can be safely validated repeatedly.
101
+ - Failed migrations retain their baseline/candidate phase instead of inferring it from
102
+ earlier progress; summaries omit Node source excerpts and provider SQL output.
103
+ - Release preparation, publication and tag repair now refuse missing or ambiguous live
104
+ repository/environment controls; prereleases preserve a stable `latest` version even
105
+ when that version has build metadata.
106
+ - Read-only release CI drift-checks an owner-observed settings record when GitHub hides
107
+ bypass metadata. Publication still requires an actual owner deployment review bound
108
+ to the exact run, attempt, source, artifact and settings-record checksums.
109
+
110
+ These changes belong to rc.5, not the existing rc.4 artifact. Scheduled review is
111
+ not unattended cleanup, and container age is not proof of user inactivity.
7
112
 
8
113
  ## [0.1.0-rc.4] - 2026-10-11
9
114
 
package/COMPATIBILITY.md CHANGED
@@ -17,7 +17,7 @@ A change to any of those requires either a compatible extension or a deliberate
17
17
  transition. Rehearsal must never silently reinterpret an old config, baseline, digest,
18
18
  or successful verification receipt.
19
19
 
20
- The first stable `1.0.0` requires external beta evidence, a support policy, and a settled
20
+ The first `1.0.0` release requires external beta evidence, a support policy, and a settled
21
21
  public API. Supporting additional database families, package managers, or operating
22
22
  systems is not implied by the 0.x contract.
23
23
 
package/README.md CHANGED
@@ -16,6 +16,7 @@
16
16
  <p align="center">
17
17
  <a href="https://ddupasquier.github.io/rehearsal-db/">Read the documentation</a> ·
18
18
  <a href="https://www.npmjs.com/package/@rehearsal-db/core">View on npm</a> ·
19
+ <a href="https://github.com/Ddupasquier/rehearsal-db">Source on GitHub</a> ·
19
20
  <a href="https://github.com/Ddupasquier/rehearsal-db/issues">Report a problem</a>
20
21
  </p>
21
22
 
@@ -129,6 +130,15 @@ When local disk space gets tight, choose **Clean up disk space** in the guide. R
129
130
  previews old baseline generations first and keeps runtime or shared-image removal
130
131
  explicit.
131
132
 
133
+ Run `npx rehearsal environments` for a read-only view of this project's owned runtimes,
134
+ container state, current owned-container CPU and memory, Docker resource totals, and
135
+ measurable Colima disk capacity. Rehearsal
136
+ warns before the disk becomes critical and refuses expensive startup without configured
137
+ headroom. PostgreSQL uses one container for SQL-only proofs; Supabase's default
138
+ `application` profile keeps application services while omitting development-only extras.
139
+ Optional idle shutdown can stop only a Colima boot Rehearsal proves it started, and only
140
+ after every container on the shared engine is already stopped.
141
+
132
142
  Press `Ctrl+Z` at any guided prompt to exit the whole session. Choose **Get help**, or run
133
143
  `npx rehearsal support`, to create a privacy-safe diagnostic report.
134
144
 
package/SECURITY.md CHANGED
@@ -4,9 +4,10 @@ Rehearsal handles sensitive database topology and may process sanitized derivati
4
4
  production data. Do not include credentials, connection strings, raw data, or a real
5
5
  baseline in a public report.
6
6
 
7
- Report suspected vulnerabilities through GitHub's private vulnerability reporting for
8
- this repository. Do not open a public issue containing an exploit, secret, hosted
9
- target, or source record.
7
+ Report suspected vulnerabilities through the
8
+ [private security form](https://github.com/Ddupasquier/rehearsal-db/security/advisories/new)
9
+ in this repository. A GitHub account is required. Do not open a
10
+ public issue containing an exploit, secret, hosted target, or source record.
10
11
 
11
12
  Supported security updates cover the latest released 0.x minor during beta. A security
12
13
  fix may deliberately tighten configuration or refuse a workflow that an earlier beta
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "0.1.0-rc.4",
3
+ "version": "0.1.0-rc.8",
4
4
  "private": false,
5
5
  "description": "Catch PostgreSQL and Supabase migrations that pass on empty databases but fail on real data—before deploy.",
6
6
  "repository": {
@@ -23,7 +23,12 @@
23
23
  "rehearsal": "dist/src/cli/rehearsal.mjs"
24
24
  },
25
25
  "rehearsalContracts": {
26
- "projectDiscovery": 1
26
+ "projectDiscovery": 1,
27
+ "resourceInventory": 1,
28
+ "resourceManager": 1,
29
+ "migrationReview": 1,
30
+ "configurationReview": 1,
31
+ "baselineShape": 1
27
32
  },
28
33
  "exports": {
29
34
  ".": {
@@ -38,7 +43,8 @@
38
43
  "./privacy": "./dist/src/baseline/privacy_engine.mjs",
39
44
  "./schema": "./dist/src/baseline/schema_snapshot.mjs",
40
45
  "./service-environment": "./dist/src/runtime/service_environment.mjs",
41
- "./source-access": "./dist/src/source/access.mjs"
46
+ "./source-access": "./dist/src/source/access.mjs",
47
+ "./shape": "./dist/src/source/shape.mjs"
42
48
  },
43
49
  "files": [
44
50
  "dist",
@@ -69,6 +75,7 @@
69
75
  "test:fixture:typescript": "npm run build && node scripts/verification/run_scenario.mjs typescript",
70
76
  "test:fixture:standalone": "npm run build && node scripts/verification/run_scenario.mjs standalone",
71
77
  "test:fixture:privacy": "npm run build && node scripts/verification/run_scenario.mjs privacy",
78
+ "test:fixture:resources": "npm run build && node scripts/verification/run_scenario.mjs resources",
72
79
  "release:verify": "node scripts/verification/release_gate.mjs",
73
80
  "check": "npm run check:syntax && npm run typecheck && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
74
81
  "check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
@@ -7,15 +7,25 @@ import { loadRehearsalConfig } from "../../src/project/configuration.mjs";
7
7
  import { parseRuntimeInvocation } from "../../src/targets/target.mjs";
8
8
  import { runRuntimeTarget } from "../../src/targets/driver.mjs";
9
9
  import { runWithOperationCancellation } from "../../src/shared/cancellation.mjs";
10
+ import { withRuntimeStartupCleanup } from "../../src/runtime/runtime_startup_ownership.mjs";
11
+ import { guardRuntimeManagerAction } from "../../src/runtime/runtime_manager_guards.mjs";
10
12
  const run = async () => {
11
13
  const invocation = parseRuntimeInvocation();
12
- const { config } = await loadRehearsalConfig({
14
+ const loaded = await loadRehearsalConfig({
13
15
  projectRoot: process.cwd(),
14
16
  ...(invocation.configPath === undefined
15
17
  ? {}
16
18
  : { configPath: invocation.configPath }),
17
19
  });
18
- await runRuntimeTarget(config.runtime.target);
20
+ await guardRuntimeManagerAction(loaded, invocation.action);
21
+ await withRuntimeStartupCleanup({
22
+ projectRoot: loaded.projectRoot,
23
+ configPath: loaded.configPath,
24
+ config: loaded.config,
25
+ action: invocation.action,
26
+ ownerOperationId: process.env.REHEARSAL_OWNER_OPERATION_ID,
27
+ task: () => runRuntimeTarget(loaded.config.runtime.target),
28
+ });
19
29
  };
20
30
  if (process.env.REHEARSAL_CANCELLABLE_OPERATION === "1") {
21
31
  await runWithOperationCancellation({ task: run });
@@ -0,0 +1,28 @@
1
+ /** Explicit application-owned activity; never infer use from container or editor age. */
2
+ export interface ApplicationActivity {
3
+ readonly url: string;
4
+ }
5
+ export interface ApplicationActivitySample {
6
+ readonly schemaVersion: 1;
7
+ readonly busy: boolean;
8
+ readonly activitySequence: number;
9
+ }
10
+ export interface ApplicationIdleWindow {
11
+ readonly sequence: number | null;
12
+ readonly quietSince: number | null;
13
+ readonly lastObservedAt: number | null;
14
+ readonly validSamples: number;
15
+ }
16
+ export declare const emptyApplicationIdleWindow: () => ApplicationIdleWindow;
17
+ export declare const validateApplicationActivityUrl: (value: unknown, readinessUrl: string | null) => string;
18
+ export declare const parseApplicationActivitySample: (value: unknown) => ApplicationActivitySample | null;
19
+ /** `now` is a monotonic clock; missing evidence cannot carry a previous quiet window. */
20
+ export declare const observeApplicationActivity: ({ previous, sample, now, timeoutMs, }: {
21
+ readonly previous: ApplicationIdleWindow;
22
+ readonly sample: ApplicationActivitySample | null;
23
+ readonly now: number;
24
+ readonly timeoutMs: number;
25
+ }) => {
26
+ readonly window: ApplicationIdleWindow;
27
+ readonly idle: boolean;
28
+ };
@@ -0,0 +1,84 @@
1
+ export const emptyApplicationIdleWindow = () => ({
2
+ sequence: null,
3
+ quietSince: null,
4
+ lastObservedAt: null,
5
+ validSamples: 0,
6
+ });
7
+ export const validateApplicationActivityUrl = (value, readinessUrl) => {
8
+ const message = "Application activity requires a bounded credential-free loopback HTTP(S) URL on the exact readiness origin, with an explicit non-privileged port and no query or fragment.";
9
+ if (typeof value !== "string" ||
10
+ value.length > 500 ||
11
+ /[\x00-\x20\x7f]/u.test(value))
12
+ throw new Error(message);
13
+ let url;
14
+ let readiness;
15
+ try {
16
+ url = new URL(value);
17
+ readiness = new URL(readinessUrl ?? "");
18
+ }
19
+ catch {
20
+ throw new Error(message);
21
+ }
22
+ if (!/^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):[0-9]+(?:\/|$)/iu.test(value) ||
23
+ !["http:", "https:"].includes(url.protocol) ||
24
+ !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) ||
25
+ url.username ||
26
+ url.password ||
27
+ url.search ||
28
+ url.hash ||
29
+ Number(url.port) < 1024 ||
30
+ Number(url.port) > 65535 ||
31
+ readiness.username ||
32
+ readiness.password ||
33
+ url.origin !== readiness.origin)
34
+ throw new Error(message);
35
+ return url.href;
36
+ };
37
+ export const parseApplicationActivitySample = (value) => {
38
+ if (!value || typeof value !== "object" || Array.isArray(value))
39
+ return null;
40
+ const sample = value;
41
+ if (Object.keys(sample).length !== 3 ||
42
+ Object.keys(sample).some((key) => !["schemaVersion", "busy", "activitySequence"].includes(key)) ||
43
+ sample.schemaVersion !== 1 ||
44
+ typeof sample.busy !== "boolean" ||
45
+ typeof sample.activitySequence !== "number" ||
46
+ !Number.isSafeInteger(sample.activitySequence) ||
47
+ sample.activitySequence < 0)
48
+ return null;
49
+ return {
50
+ schemaVersion: 1,
51
+ busy: sample.busy,
52
+ activitySequence: sample.activitySequence,
53
+ };
54
+ };
55
+ /** `now` is a monotonic clock; missing evidence cannot carry a previous quiet window. */
56
+ export const observeApplicationActivity = ({ previous, sample, now, timeoutMs, }) => {
57
+ if (!Number.isFinite(now) ||
58
+ !Number.isFinite(timeoutMs) ||
59
+ timeoutMs <= 0 ||
60
+ (previous.lastObservedAt !== null && now < previous.lastObservedAt) ||
61
+ !sample ||
62
+ (previous.sequence !== null && sample.activitySequence < previous.sequence))
63
+ return {
64
+ window: { ...previous, quietSince: null, validSamples: 0 },
65
+ idle: false,
66
+ };
67
+ const changed = previous.sequence === null || sample.activitySequence > previous.sequence;
68
+ const reset = changed || sample.busy || previous.quietSince === null;
69
+ const quietSince = sample.busy ? null : reset ? now : previous.quietSince;
70
+ const validSamples = sample.busy
71
+ ? 0
72
+ : reset
73
+ ? 1
74
+ : Math.min(previous.validSamples + 1, 2);
75
+ return {
76
+ window: {
77
+ sequence: sample.activitySequence,
78
+ quietSince,
79
+ validSamples,
80
+ lastObservedAt: now,
81
+ },
82
+ idle: quietSince !== null && validSamples >= 2 && now - quietSince >= timeoutMs,
83
+ };
84
+ };
@@ -0,0 +1,20 @@
1
+ import { type ApplicationActivitySample } from "./activity_contract.mjs";
2
+ export declare const readApplicationActivity: ({ url, signal, fetchImplementation, timeoutMs, }: {
3
+ readonly url: string;
4
+ readonly signal: AbortSignal;
5
+ readonly fetchImplementation?: typeof fetch;
6
+ readonly timeoutMs?: number;
7
+ }) => Promise<ApplicationActivitySample | null>;
8
+ export declare const waitForApplicationIdle: ({ url, readinessUrl, timeoutSeconds, signal, fetchImplementation, now, pollIntervalMs, requestTimeoutMs, onAvailability, }: {
9
+ readonly url: string;
10
+ readonly readinessUrl: string;
11
+ readonly timeoutSeconds: number;
12
+ readonly signal: AbortSignal;
13
+ readonly fetchImplementation?: typeof fetch;
14
+ readonly now?: () => number;
15
+ readonly pollIntervalMs?: number;
16
+ readonly requestTimeoutMs?: number;
17
+ readonly onAvailability?: (available: boolean) => void;
18
+ }) => Promise<Readonly<{
19
+ reason: "idle-timeout";
20
+ }>>;
@@ -0,0 +1,191 @@
1
+ /** Foreground-only serial polling of an explicitly declared application activity route. */
2
+ import { performance } from "node:perf_hooks";
3
+ import { emptyApplicationIdleWindow, observeApplicationActivity, parseApplicationActivitySample, validateApplicationActivityUrl, } from "./activity_contract.mjs";
4
+ const MAXIMUM_ACTIVITY_BYTES = 4096;
5
+ const aborted = () => Object.assign(new Error("Application activity monitoring stopped."), {
6
+ name: "AbortError",
7
+ });
8
+ class ActivityTransportUnresponsive extends Error {
9
+ constructor() {
10
+ super("Application activity transport did not stop within its bound; idle shutdown is disabled for this foreground session.");
11
+ }
12
+ }
13
+ const settleWithin = async (promise, milliseconds) => {
14
+ let timer;
15
+ try {
16
+ return await Promise.race([
17
+ promise.then(() => true, () => true),
18
+ new Promise((resolve) => {
19
+ timer = setTimeout(() => resolve(false), milliseconds);
20
+ }),
21
+ ]);
22
+ }
23
+ finally {
24
+ if (timer)
25
+ clearTimeout(timer);
26
+ }
27
+ };
28
+ const wait = (milliseconds, signal) => new Promise((resolve, reject) => {
29
+ if (signal.aborted) {
30
+ reject(aborted());
31
+ return;
32
+ }
33
+ const finish = (cancelled) => {
34
+ clearTimeout(timer);
35
+ signal.removeEventListener("abort", onAbort);
36
+ if (cancelled)
37
+ reject(aborted());
38
+ else
39
+ resolve();
40
+ };
41
+ const onAbort = () => finish(true);
42
+ const timer = setTimeout(() => finish(false), milliseconds);
43
+ signal.addEventListener("abort", onAbort, { once: true });
44
+ });
45
+ export const readApplicationActivity = async ({ url, signal, fetchImplementation = fetch, timeoutMs = 2000, }) => {
46
+ if (signal.aborted)
47
+ throw aborted();
48
+ const started = performance.now();
49
+ const budget = Math.min(Math.max(timeoutMs, 1), 2000);
50
+ const request = new AbortController();
51
+ let rejectOwner = () => undefined;
52
+ const ownerAborted = new Promise((_resolve, reject) => {
53
+ rejectOwner = reject;
54
+ });
55
+ const onAbort = () => {
56
+ request.abort();
57
+ rejectOwner(aborted());
58
+ };
59
+ signal.addEventListener("abort", onAbort, { once: true });
60
+ const abortTimer = setTimeout(() => request.abort(), Math.max(1, budget - 50));
61
+ let deadlineTimer;
62
+ const deadline = new Promise((_resolve, reject) => {
63
+ deadlineTimer = setTimeout(() => {
64
+ request.abort();
65
+ reject(new ActivityTransportUnresponsive());
66
+ }, budget);
67
+ });
68
+ let reader;
69
+ const read = async () => {
70
+ try {
71
+ const response = await fetchImplementation(url, {
72
+ signal: request.signal,
73
+ method: "GET",
74
+ redirect: "error",
75
+ headers: { accept: "application/json", "cache-control": "no-store" },
76
+ });
77
+ if (request.signal.aborted)
78
+ return null;
79
+ if (response.status !== 200 ||
80
+ response.redirected ||
81
+ !/^application\/json(?:\s*;|$)/iu.test(response.headers.get("content-type") ?? "") ||
82
+ !response.body)
83
+ return null;
84
+ const declared = response.headers.get("content-length");
85
+ if (declared !== null &&
86
+ (!/^[0-9]+$/u.test(declared) ||
87
+ Number(declared) > MAXIMUM_ACTIVITY_BYTES))
88
+ return null;
89
+ reader = response.body.getReader();
90
+ const chunks = [];
91
+ let length = 0;
92
+ while (true) {
93
+ if (request.signal.aborted)
94
+ return null;
95
+ const result = await reader.read();
96
+ if (request.signal.aborted)
97
+ return null;
98
+ if (result.done)
99
+ break;
100
+ length += result.value.byteLength;
101
+ if (length > MAXIMUM_ACTIVITY_BYTES)
102
+ return null;
103
+ chunks.push(result.value);
104
+ }
105
+ const bytes = Buffer.concat(chunks, length);
106
+ return parseApplicationActivitySample(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ };
112
+ try {
113
+ return await Promise.race([read(), deadline, ownerAborted]);
114
+ }
115
+ finally {
116
+ clearTimeout(abortTimer);
117
+ if (deadlineTimer)
118
+ clearTimeout(deadlineTimer);
119
+ signal.removeEventListener("abort", onAbort);
120
+ request.abort();
121
+ const cancelled = reader?.cancel().catch(() => undefined);
122
+ const cancellationFinished = cancelled === undefined ||
123
+ (await settleWithin(cancelled, Math.max(1, Math.min(50, budget - (performance.now() - started)))));
124
+ try {
125
+ reader?.releaseLock();
126
+ }
127
+ catch {
128
+ /* A stuck transport is disabled below. */
129
+ }
130
+ if (signal.aborted)
131
+ throw aborted();
132
+ if (!cancellationFinished)
133
+ throw new ActivityTransportUnresponsive();
134
+ }
135
+ };
136
+ export const waitForApplicationIdle = async ({ url, readinessUrl, timeoutSeconds, signal, fetchImplementation = fetch, now = () => performance.now(), pollIntervalMs = 5000, requestTimeoutMs = 2000, onAvailability = () => undefined, }) => {
137
+ const endpoint = validateApplicationActivityUrl(url, readinessUrl);
138
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)
139
+ throw new Error("Application idle monitoring requires an explicit positive timeout.");
140
+ let window = emptyApplicationIdleWindow();
141
+ let available;
142
+ while (!signal.aborted) {
143
+ let sample;
144
+ try {
145
+ sample = await readApplicationActivity({
146
+ url: endpoint,
147
+ signal,
148
+ fetchImplementation,
149
+ timeoutMs: requestTimeoutMs,
150
+ });
151
+ }
152
+ catch (error) {
153
+ if (signal.aborted)
154
+ throw aborted();
155
+ if (!(error instanceof ActivityTransportUnresponsive))
156
+ throw error;
157
+ onAvailability(false);
158
+ // Do not accumulate new requests behind a transport that ignored abort/cancel.
159
+ await new Promise((_resolve, reject) => {
160
+ const stop = () => reject(aborted());
161
+ signal.addEventListener("abort", stop, { once: true });
162
+ if (signal.aborted) {
163
+ signal.removeEventListener("abort", stop);
164
+ stop();
165
+ }
166
+ });
167
+ throw aborted();
168
+ }
169
+ if (signal.aborted)
170
+ throw aborted();
171
+ const current = observeApplicationActivity({
172
+ previous: window,
173
+ sample,
174
+ now: now(),
175
+ timeoutMs: timeoutSeconds * 1000,
176
+ });
177
+ window = current.window;
178
+ const nextAvailable = sample !== null &&
179
+ (window.sequence === null ||
180
+ sample.activitySequence >= window.sequence) &&
181
+ window.lastObservedAt !== null;
182
+ if (available !== nextAvailable) {
183
+ available = nextAvailable;
184
+ onAvailability(available);
185
+ }
186
+ if (current.idle)
187
+ return { reason: "idle-timeout" };
188
+ await wait(Math.max(1, pollIntervalMs), signal);
189
+ }
190
+ throw aborted();
191
+ };
@@ -0,0 +1,12 @@
1
+ export interface LocalApplicationEndpoint {
2
+ readonly host: "localhost" | "127.0.0.1" | "::1";
3
+ readonly port: number;
4
+ }
5
+ type ProbeState = "free" | "occupied" | "unavailable" | "unsupported";
6
+ export type ApplicationPortProbe = (host: "127.0.0.1" | "::1", port: number) => Promise<ProbeState>;
7
+ export declare const parseLocalApplicationEndpoint: (value: unknown) => LocalApplicationEndpoint;
8
+ export declare const assertApplicationPortAvailable: ({ url, probe, }: {
9
+ readonly url: unknown;
10
+ readonly probe?: ApplicationPortProbe;
11
+ }) => Promise<void>;
12
+ export {};
@@ -0,0 +1,71 @@
1
+ /** Startup-only, bounded loopback listener check; never touches an existing process. */
2
+ import { createConnection } from "node:net";
3
+ export const parseLocalApplicationEndpoint = (value) => {
4
+ let parsed;
5
+ try {
6
+ if (typeof value !== "string")
7
+ throw new Error();
8
+ parsed = new URL(value);
9
+ }
10
+ catch {
11
+ throw new Error("Application readiness requires a valid local HTTP or HTTPS endpoint.");
12
+ }
13
+ const host = parsed.hostname === "[::1]" ? "::1" : parsed.hostname;
14
+ const port = Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
15
+ if (!["http:", "https:"].includes(parsed.protocol) ||
16
+ !["localhost", "127.0.0.1", "::1"].includes(host) ||
17
+ parsed.username ||
18
+ parsed.password ||
19
+ !Number.isSafeInteger(port) ||
20
+ port < 1 ||
21
+ port > 65535)
22
+ throw new Error("Application readiness requires a credential-free loopback HTTP or HTTPS endpoint.");
23
+ return Object.freeze({
24
+ host: host,
25
+ port,
26
+ });
27
+ };
28
+ const probeLoopbackPort = (host, port) => new Promise((resolve) => {
29
+ const socket = createConnection({
30
+ host,
31
+ port,
32
+ family: host === "::1" ? 6 : 4,
33
+ });
34
+ let settled = false;
35
+ const finish = (state) => {
36
+ if (settled)
37
+ return;
38
+ settled = true;
39
+ clearTimeout(timer);
40
+ socket.destroy();
41
+ resolve(state);
42
+ };
43
+ const timer = setTimeout(() => finish("unavailable"), 750);
44
+ socket.once("connect", () => finish("occupied"));
45
+ socket.once("error", (error) => finish(error.code === "ECONNREFUSED"
46
+ ? "free"
47
+ : [
48
+ "EAFNOSUPPORT",
49
+ "EPROTONOSUPPORT",
50
+ "ENETUNREACH",
51
+ "EHOSTUNREACH",
52
+ ].includes(error.code ?? "") && host === "::1"
53
+ ? "unsupported"
54
+ : "unavailable"));
55
+ });
56
+ export const assertApplicationPortAvailable = async ({ url, probe = probeLoopbackPort, }) => {
57
+ const endpoint = parseLocalApplicationEndpoint(url);
58
+ const hosts = endpoint.host === "localhost" ? ["127.0.0.1", "::1"] : [endpoint.host];
59
+ let results;
60
+ try {
61
+ results = await Promise.all(hosts.map((host) => probe(host, endpoint.port)));
62
+ }
63
+ catch {
64
+ throw new Error("Application port availability could not be proved. No application was started; diagnose the local endpoint before retrying.");
65
+ }
66
+ if (results.includes("occupied"))
67
+ throw new Error(`Application port ${endpoint.port} is already in use. No application was started or stopped. Stop the existing application yourself, or choose a different reviewed local port and update its start command.`);
68
+ if (results.some((state) => !["free", "unsupported"].includes(state)) ||
69
+ !results.includes("free"))
70
+ throw new Error("Application port availability could not be proved. No application was started; diagnose the local endpoint before retrying.");
71
+ };