@zq-silk/yui 0.6.0 → 0.6.2

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 (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Read-only runtime identity facts for `controller status` and `execution audit`.
3
+ *
4
+ * Issue 11 boundary: this module only collects facts (manifest, process
5
+ * realpath/env, on-disk store evidence, Git). It never mutates state, never
6
+ * starts or stops a Controller, and never asks a producer to change. A fact
7
+ * that cannot be resolved is the literal string `"unsupported"` (the Issue 11
8
+ * fallback rule), and contradictory facts fail closed as findings with exact
9
+ * remediation actions.
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { spawnSync } from "node:child_process";
13
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import Database from "better-sqlite3";
16
+ import { CURRENT_AGGREGATE_SCHEMA_VERSION, CURRENT_STORAGE_LAYOUT_VERSION } from "../storage/storageVersions.js";
17
+ import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
18
+ import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
19
+ export const UNSUPPORTED = "unsupported";
20
+ /**
21
+ * Feature flag for the Issue 11 read-only status/audit presentation.
22
+ *
23
+ * Everything behind this flag is observe-only: identity fields, storage
24
+ * evidence, and metrics snapshots. The flag defaults ON because the fields
25
+ * change no behavior; set `YUI_STATUS_IDENTITY=0` (or `false`) to hide the new
26
+ * sections and the fail-closed health exit. `execution audit` is an explicit
27
+ * command and is not gated by this flag.
28
+ */
29
+ export function resolveStatusIdentityEnabled(env = process.env) {
30
+ const value = env.YUI_STATUS_IDENTITY;
31
+ if (value === undefined)
32
+ return true;
33
+ const normalized = value.trim().toLowerCase();
34
+ return normalized !== "0" && normalized !== "false" && normalized !== "off";
35
+ }
36
+ export function evaluateStorageHealth(identity) {
37
+ const contradictions = identity.findings.filter(({ severity }) => severity === "contradiction");
38
+ const needsRepair = identity.findings.filter(({ severity }) => severity === "needs-repair");
39
+ const warnings = identity.findings.filter(({ severity }) => severity === "warning");
40
+ const status = contradictions.length > 0
41
+ ? "fail"
42
+ : needsRepair.length > 0
43
+ ? "degraded"
44
+ : "ok";
45
+ return { status, healthy: status === "ok", contradictions, needsRepair, warnings };
46
+ }
47
+ export function createProductionRuntimeIdentityPorts(packageRoot, entryPath, env = process.env) {
48
+ return {
49
+ env,
50
+ packageRoot,
51
+ entryPath,
52
+ readText: (path) => {
53
+ try {
54
+ return readFileSync(path, "utf8");
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ },
60
+ fileSize: (path) => {
61
+ try {
62
+ return statSync(path).size;
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ },
68
+ exists: (path) => existsSync(path),
69
+ realpath: (path) => {
70
+ try {
71
+ return realpathSync(path);
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ },
77
+ gitHead: (root) => readGitHead(root)
78
+ };
79
+ }
80
+ function readGitHead(packageRoot) {
81
+ try {
82
+ const result = spawnSync("git", ["-C", packageRoot, "rev-parse", "HEAD"], { encoding: "utf8", timeout: 1_500, stdio: ["ignore", "pipe", "ignore"] });
83
+ if (result.status !== 0)
84
+ return null;
85
+ const head = result.stdout.trim();
86
+ return /^[a-f0-9]{7,40}$/u.test(head) ? head : null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ function sha256Hex(content) {
93
+ return createHash("sha256").update(content).digest("hex");
94
+ }
95
+ export function collectRuntimeBuildIdentity(ports) {
96
+ const packageJsonText = ports.readText(join(ports.packageRoot, "package.json"));
97
+ let packageName = "unknown";
98
+ let packageVersion = "0.0.0";
99
+ let packageDigest = UNSUPPORTED;
100
+ if (packageJsonText !== null) {
101
+ packageDigest = `sha256:${sha256Hex(packageJsonText)}`;
102
+ try {
103
+ const parsed = JSON.parse(packageJsonText);
104
+ if (typeof parsed.name === "string" && parsed.name.length > 0) {
105
+ packageName = parsed.name;
106
+ }
107
+ if (typeof parsed.version === "string" && parsed.version.length > 0) {
108
+ packageVersion = parsed.version;
109
+ }
110
+ }
111
+ catch {
112
+ // Damaged package metadata still leaves the digest as evidence.
113
+ }
114
+ }
115
+ const entryRealpath = ports.realpath(ports.entryPath);
116
+ const entryText = entryRealpath === null ? null : ports.readText(entryRealpath);
117
+ return {
118
+ packageName,
119
+ packageVersion,
120
+ packageDigest,
121
+ entryPath: entryRealpath ?? UNSUPPORTED,
122
+ entryDigest: entryText === null
123
+ ? UNSUPPORTED
124
+ : `sha256:${sha256Hex(entryText)}`,
125
+ sourceCommit: ports.gitHead(ports.packageRoot) ?? UNSUPPORTED,
126
+ nodeVersion: process.version,
127
+ platform: `${process.platform}/${process.arch}`
128
+ };
129
+ }
130
+ export function createProductionStorageIdentityPorts(env = process.env) {
131
+ return {
132
+ env,
133
+ readText: (path) => {
134
+ try {
135
+ return readFileSync(path, "utf8");
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ },
141
+ fileSize: (path) => {
142
+ try {
143
+ return statSync(path).size;
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ },
149
+ exists: (path) => existsSync(path),
150
+ probeDatabaseHealth: (path) => {
151
+ if (!existsSync(path))
152
+ return null;
153
+ try {
154
+ const db = new Database(path, { readonly: true });
155
+ try {
156
+ const integrity = db.pragma("quick_check", { simple: true });
157
+ return integrity === "ok" ? "ok" : "corrupt";
158
+ }
159
+ finally {
160
+ db.close();
161
+ }
162
+ }
163
+ catch {
164
+ return "unopenable";
165
+ }
166
+ }
167
+ };
168
+ }
169
+ /**
170
+ * Collect durable-vs-physical storage evidence for one Home. The manifest
171
+ * declares a logical layout; the directory carries physical evidence
172
+ * (state.json vs yui.db + WAL). Contradictions between the two are the
173
+ * pseudo-layout-7 class of failure and fail closed.
174
+ */
175
+ export function collectStorageIdentity(home, ports = createProductionStorageIdentityPorts()) {
176
+ const manifestPath = join(home, "schema.json");
177
+ const manifestText = ports.readText(manifestPath);
178
+ let manifestStatus = "uninitialized";
179
+ let logicalLayout = UNSUPPORTED;
180
+ let aggregateSchemaVersion = UNSUPPORTED;
181
+ if (manifestText !== null) {
182
+ manifestStatus = "current";
183
+ try {
184
+ const manifest = JSON.parse(manifestText);
185
+ if (typeof manifest.storageVersion === "number"
186
+ && Number.isFinite(manifest.storageVersion)) {
187
+ logicalLayout = manifest.storageVersion;
188
+ }
189
+ if (typeof manifest.aggregateSchemaVersion === "number"
190
+ && Number.isFinite(manifest.aggregateSchemaVersion)) {
191
+ aggregateSchemaVersion = manifest.aggregateSchemaVersion;
192
+ }
193
+ if (typeof logicalLayout === "number"
194
+ && (logicalLayout > CURRENT_STORAGE_LAYOUT_VERSION
195
+ || (typeof aggregateSchemaVersion === "number"
196
+ && aggregateSchemaVersion > CURRENT_AGGREGATE_SCHEMA_VERSION))) {
197
+ manifestStatus = "unsupported";
198
+ }
199
+ }
200
+ catch {
201
+ manifestStatus = "invalid";
202
+ logicalLayout = UNSUPPORTED;
203
+ aggregateSchemaVersion = UNSUPPORTED;
204
+ }
205
+ }
206
+ const statePath = join(home, "state.json");
207
+ const statePresent = ports.exists(statePath);
208
+ const stateBytes = ports.fileSize(statePath);
209
+ const dbPath = join(home, "yui.db");
210
+ const dbPresent = ports.exists(dbPath);
211
+ const dbBytes = ports.fileSize(dbPath);
212
+ const walPresent = ports.exists(`${dbPath}-wal`);
213
+ const shmPresent = ports.exists(`${dbPath}-shm`);
214
+ const configuredBackend = resolveTaskStoreBackendForHome(home, ports.env);
215
+ const workerEnabled = resolveStoreWorkerEnabledForHome(home, ports.env);
216
+ const dbHealth = dbPresent ? ports.probeDatabaseHealth(dbPath) : null;
217
+ const hasMigrationReceipt = ports.exists(join(home, "migration-receipt.json"));
218
+ const findings = [];
219
+ // A readable state.json is a JSON object (the file store's record container).
220
+ // Pseudo-layout-7 is only repairable when state.json is a valid fallback.
221
+ const stateText = statePresent ? ports.readText(statePath) : null;
222
+ let stateReadable = false;
223
+ if (stateText !== null) {
224
+ try {
225
+ const parsed = JSON.parse(stateText);
226
+ stateReadable = typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
227
+ }
228
+ catch {
229
+ stateReadable = false;
230
+ }
231
+ }
232
+ const layout7 = manifestStatus === "current"
233
+ && typeof logicalLayout === "number"
234
+ && logicalLayout >= CURRENT_STORAGE_LAYOUT_VERSION;
235
+ if (layout7 && !dbPresent) {
236
+ if (stateReadable) {
237
+ // Pseudo-layout-7: manifest claims SQLite WAL but yui.db was never
238
+ // materialized. state.json is the readable fallback; `yui upgrade`
239
+ // rebuilds yui.db from it. This is the recognized NEEDS_STORAGE_REPAIR
240
+ // state, not a hard contradiction — the file store is legitimately
241
+ // serving until repair runs.
242
+ findings.push({
243
+ code: "pseudo-layout-7",
244
+ severity: "needs-repair",
245
+ message: `Manifest declares layout ${logicalLayout} (SQLite WAL) but `
246
+ + `${dbPath} does not exist; state.json is the only authoritative `
247
+ + "copy (pseudo-layout-7).",
248
+ remediation: "Run `yui upgrade` to rebuild yui.db from state.json."
249
+ });
250
+ }
251
+ else {
252
+ findings.push({
253
+ code: "no-authoritative-backend",
254
+ severity: "contradiction",
255
+ message: `Manifest declares layout ${logicalLayout} but neither `
256
+ + "yui.db nor a readable state.json exists; there is no "
257
+ + "authoritative backend.",
258
+ remediation: "Restore one copy from a backup; do not guess which is newer."
259
+ });
260
+ }
261
+ }
262
+ if (dbPresent && dbHealth !== null && dbHealth !== "ok") {
263
+ findings.push({
264
+ code: "database-unhealthy",
265
+ severity: "contradiction",
266
+ message: `yui.db exists but failed integrity check (${dbHealth}).`,
267
+ remediation: "Restore yui.db from backup, or run `yui upgrade` to rebuild "
268
+ + "it from state.json."
269
+ });
270
+ }
271
+ if (dbPresent && statePresent && !hasMigrationReceipt && layout7) {
272
+ findings.push({
273
+ code: "dual-copy-conflict",
274
+ severity: "contradiction",
275
+ message: "Both state.json and yui.db exist without a migration receipt; "
276
+ + "the authoritative copy is ambiguous.",
277
+ remediation: "Restore one copy from a backup; do not guess which is newer."
278
+ });
279
+ }
280
+ if (configuredBackend === "sqlite" && !dbPresent) {
281
+ findings.push({
282
+ code: "backend-sqlite-without-database",
283
+ severity: "contradiction",
284
+ message: "YUI_STORE_BACKEND=sqlite is set but no yui.db exists; the "
285
+ + "SQLite store cannot open this Home.",
286
+ remediation: "Run `yui upgrade` to create yui.db, or unset "
287
+ + "YUI_STORE_BACKEND to keep using the file store explicitly."
288
+ });
289
+ }
290
+ if (configuredBackend === "file"
291
+ && dbPresent
292
+ && manifestStatus === "current") {
293
+ findings.push({
294
+ code: "database-present-but-file-backend",
295
+ severity: "warning",
296
+ message: "yui.db exists but the file backend is selected; this process "
297
+ + "is serving state.json, not the database.",
298
+ remediation: "Unset YUI_STORE_BACKEND to let the Home manifest select "
299
+ + "SQLite, or remove the stale yui.db."
300
+ });
301
+ }
302
+ const rawWorkerFlag = ports.env.YUI_STORE_WORKER;
303
+ if (rawWorkerFlag !== undefined
304
+ && !workerEnabled
305
+ && configuredBackend !== "sqlite") {
306
+ findings.push({
307
+ code: "worker-flag-without-sqlite",
308
+ severity: "warning",
309
+ message: `YUI_STORE_WORKER=${rawWorkerFlag} is set but the worker `
310
+ + "requires YUI_STORE_BACKEND=sqlite; the worker is off.",
311
+ remediation: "Set YUI_STORE_BACKEND=sqlite to activate the worker, or "
312
+ + "unset YUI_STORE_WORKER."
313
+ });
314
+ }
315
+ // Only fire for pre-layout-7 Homes where the file store is the legitimate
316
+ // backend. A layout-7 Home without state.json is already covered by
317
+ // no-authoritative-backend above (the file store is just a fallback there).
318
+ if (configuredBackend === "file"
319
+ && !statePresent
320
+ && manifestStatus === "current"
321
+ && !(typeof logicalLayout === "number" && logicalLayout >= CURRENT_STORAGE_LAYOUT_VERSION)) {
322
+ findings.push({
323
+ code: "file-store-missing-state",
324
+ severity: "contradiction",
325
+ message: "The file backend is selected but state.json is missing.",
326
+ remediation: "Restore state.json from backup, or run the storage "
327
+ + "migration to create yui.db."
328
+ });
329
+ }
330
+ return {
331
+ home,
332
+ manifestStatus,
333
+ logicalLayout,
334
+ aggregateSchemaVersion,
335
+ configuredBackend,
336
+ workerEnabled,
337
+ physicalStateJson: {
338
+ present: statePresent,
339
+ bytes: stateBytes ?? UNSUPPORTED
340
+ },
341
+ physicalDatabase: {
342
+ present: dbPresent,
343
+ bytes: dbBytes ?? UNSUPPORTED,
344
+ wal: walPresent,
345
+ shm: shmPresent,
346
+ health: dbHealth ?? UNSUPPORTED
347
+ },
348
+ hasMigrationReceipt,
349
+ findings
350
+ };
351
+ }
352
+ /**
353
+ * Count telemetry events the runtime inbox rejected into
354
+ * `runtime/inbox-invalid/`. Dropped telemetry must stay countable and must
355
+ * never affect the semantic lane (Issue 11 §4). A missing directory means no
356
+ * drops (the inbox was never used); an unreadable directory is `unsupported`.
357
+ */
358
+ export function countDroppedInboxEvents(home, listDirectory = (path) => readdirSync(path)) {
359
+ if (!existsSync(join(home, "runtime", "inbox-invalid")))
360
+ return 0;
361
+ try {
362
+ return listDirectory(join(home, "runtime", "inbox-invalid")).length;
363
+ }
364
+ catch {
365
+ return UNSUPPORTED;
366
+ }
367
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The deterministic double for every external system (GitHub, npm, Home,
3
+ * Controller). No real network, process, or filesystem side effect: the
4
+ * engine's at-most-once and recovery guarantees are proven against this.
5
+ */
6
+ export function createFakeReleasePorts(scripts) {
7
+ const counters = new Map();
8
+ for (const stepId of Object.keys(scripts)) {
9
+ counters.set(stepId, { execute: 0, query: 0, keys: [] });
10
+ }
11
+ const tally = (stepId) => {
12
+ let calls = counters.get(stepId);
13
+ if (calls === undefined) {
14
+ calls = { execute: 0, query: 0, keys: [] };
15
+ counters.set(stepId, calls);
16
+ }
17
+ return calls;
18
+ };
19
+ const next = (queue, stepId, kind, consumed) => {
20
+ if (queue === undefined || consumed >= queue.length) {
21
+ throw new Error(`Fake release ports: no more scripted ${kind} outcomes for step ${stepId}.`);
22
+ }
23
+ const entry = queue[consumed];
24
+ return typeof entry === "function" ? entry() : entry;
25
+ };
26
+ return {
27
+ async executeStep(input) {
28
+ const calls = tally(input.step.id);
29
+ const effect = next(scripts[input.step.id]?.execute, input.step.id, "execute", calls.execute);
30
+ calls.execute += 1;
31
+ calls.keys.push(input.idempotencyKey);
32
+ return effect;
33
+ },
34
+ async queryStepEffect(input) {
35
+ const calls = tally(input.step.id);
36
+ const query = next(scripts[input.step.id]?.query, input.step.id, "query", calls.query);
37
+ calls.query += 1;
38
+ return query;
39
+ },
40
+ calls(stepId) {
41
+ const calls = tally(stepId);
42
+ return { execute: calls.execute, query: calls.query, keys: Object.freeze([...calls.keys]) };
43
+ },
44
+ exhausted() {
45
+ for (const [stepId, script] of Object.entries(scripts)) {
46
+ const calls = counters.get(stepId);
47
+ if ((script.execute?.length ?? 0) !== calls.execute)
48
+ return false;
49
+ if ((script.query?.length ?? 0) !== calls.query)
50
+ return false;
51
+ }
52
+ return true;
53
+ }
54
+ };
55
+ }