From d68480b93db26d3754ec6d527704079c1278ee0a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:27:54 +0200 Subject: [PATCH 001/177] docs(v0.13): define reconciled State Engine V2 authority --- ...dkf-v0.13-state-engine-v2-specification.md | 330 ++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 docs/04-architecture/dkf-v0.13-state-engine-v2-specification.md diff --git a/docs/04-architecture/dkf-v0.13-state-engine-v2-specification.md b/docs/04-architecture/dkf-v0.13-state-engine-v2-specification.md new file mode 100644 index 000000000..8f2132393 --- /dev/null +++ b/docs/04-architecture/dkf-v0.13-state-engine-v2-specification.md @@ -0,0 +1,330 @@ +# Development Kit v0.13.0 — State Engine V2 Specification + +**Status:** Implementation authority for v0.13 T04 +**Date:** 14 October 2026 +**Base release:** Development Kit v0.12.1 +**Priority:** P0 / first v0.13 increment +**Deferred source:** DKF120-T04 from the v0.12 Scale, Context & Iteration PLAN +**Binding release decision:** Issue #82 +**Blocking Windows recovery issue:** Issue #73 + +## 1. Objective + +Complete and independently accept the State Engine V2 work deliberately deferred from v0.12.0, while preserving all released v0.12.1 public commands, lifecycle behavior, gate semantics, verification extensions, discovery/re-entry behavior, Control Center projections and stable-state compatibility. + +State Engine V2 SHALL become the canonical persistent development-state authority after a verified migration/cutover. + +It SHALL NOT become application, business or domain state. + +## 2. General-purpose architecture rule + +DKF core stores generic control-plane state only. + +The implementation MUST NOT introduce: + +- application-specific entities; +- industry-specific lifecycle policy; +- provider-specific state semantics; +- project business-status interpretation; +- target-name-based behavior. + +Project/domain state remains outside canonical DKF development-state authority unless a generic extension explicitly persists opaque project-owned data under an approved contract. + +## 3. Required canonical layout + +State Engine V2 uses: + +```text +.development-kit/state/ + events.jsonl + snapshot.json + index.db + schema.json + pending-commit.json # recovery aid, not the sole durability witness + state.lock + migration-complete.json + legacy-backup.json +``` + +Authority order: + +1. `events.jsonl` — canonical append-only development-state history. +2. `snapshot.json` — materialized current view, rebuildable. +3. `index.db` — disposable/rebuildable acceleration index. +4. Legacy state — recovery authority only until verified migration/cutover. +5. `pending-commit.json` — recovery intent/witness; absence alone MUST NOT make a torn canonical ledger unrecoverable. + +SQLite MUST NOT be the sole source of truth. + +## 4. Acceptance criteria restored for v0.13 + +The v0.12 release explicitly deferred these criteria; v0.13 reactivates them unchanged in intent. + +### DKF-120-AC-012 +State history is append-only and canonical, with deterministic event ordering and integrity chaining. + +### DKF-120-AC-013 +Current state is materialized from canonical history and can be rebuilt deterministically. + +### DKF-120-AC-014 +The acceleration index can be deleted/corrupted and rebuilt without changing semantic state. + +### DKF-120-AC-015 +Supported legacy state migrates idempotently, fail-closed, recoverably and semantically equivalently before cutover. + +### DKF-120-AC-034 +Canonical transition-state file growth is bounded and no longer proportional to normal state-transition count. + +## 5. v0.13 compatibility requirements + +### INV-130-001 — v0.12.1 behavior preservation +All released v0.12.1 commands and included T01–T03/T05–T10 behavior remain supported. + +### INV-130-002 — no silent cutover +A project with legacy state MUST NOT switch to V2 until migration has succeeded and a migration-complete witness exists. + +### INV-130-003 — legacy recovery retained +Legacy state and backup material MUST remain recoverable until verified cutover and explicit later cleanup policy. + +### INV-130-004 — fresh-project V2 +A fresh project with no legacy persistent workflow state uses State Engine V2 directly. + +### INV-130-005 — acceptance semantics unchanged +Changing persistence authority MUST NOT weaken Development Contracts, source staleness, gate profiles, verification isolation, reviewer requirements, human approval or deterministic acceptance. + +### INV-130-006 — current feature integration +The v0.12.1 Lifecycle Instance, discovery/re-entry, gate-profile, verification-extension, complexity and Control Center code must continue to operate against their released interfaces. Persistence adapters may change underneath them; feature semantics may not silently change. + +## 6. Runtime commit durability contract + +### 6.1 Canonical event append + +Normal mutation is: + +```text +lock + -> recover any prior interrupted commit + -> validate canonical prefix + -> create pending recovery intent + -> append event suffix physically + -> flush canonical ledger + -> materialize snapshot/index + -> verify derived state against canonical history + -> acknowledge/retire pending intent +unlock +``` + +A normal append MUST NOT rewrite the entire historical ledger. + +### 6.2 Pending journal is not the only witness + +Issue #73 identified that a Windows namespace update for `pending-commit.json` cannot be treated as universally power-loss durable merely because a directory flush syscall returns successfully. + +Therefore v0.13 SHALL additionally make the canonical ledger self-recovering for an unambiguous torn **trailing append** even when the pending journal is absent. + +Allowed automatic repair: + +- incomplete/invalid final byte suffix after the last complete newline; +- a complete final JSON event whose canonical bytes are present but final newline is missing, provided event schema/hash-chain validation succeeds; +- only the final append boundary. + +Automatic repair MUST NOT: + +- discard a complete but hash-invalid event; +- repair corruption in the middle of history; +- accept sequence gaps; +- accept previous-hash mismatch; +- rewrite an arbitrary historical prefix. + +Any ambiguous corruption fails closed. + +### 6.3 Windows persistence statement + +DKF distinguishes: + +1. documented OS/API semantics; +2. deterministic fault-injection behavior; +3. actual power-loss/hardware persistence evidence. + +A successful `FlushFileBuffers`, `fsyncSync`, or `MoveFileExW(MOVEFILE_WRITE_THROUGH)` call MUST NOT be described as universal hardware power-loss proof. + +Microsoft documents that: + +- directory handles require `CreateFile(... FILE_FLAG_BACKUP_SEMANTICS ...)`; +- `FILE_FLAG_WRITE_THROUGH` requests persistent-media write-through and causes NTFS metadata changes such as rename processing to be flushed, subject to hardware support; +- `FlushFileBuffers` flushes buffered information for the supplied handle/device; +- `MOVEFILE_WRITE_THROUGH` explicitly guarantees flushing for copy/delete moves, and MUST NOT be over-generalized as proof of every same-volume directory rename transaction. + +v0.13 tests and documentation must state these limits. + +## 7. Windows legacy-backup restore/recovery contract + +Full Windows restore/recovery support remains required by the Product Owner decision recorded under Issue #73. + +A blanket Windows disable is prohibited. + +### 7.1 Supported transaction model + +Restore uses a stable sibling recovery anchor outside the directory being replaced. + +The transaction is monotonic: + +```text +PREPARED +ORIGINAL_DISPLACED +CANDIDATE_PUBLISHED +PROMOTION_ACKNOWLEDGED +CLEANUP_SAFE +``` + +Each phase has authenticated path/inode/content witnesses. + +The durable recovery anchor MUST NOT be deleted as the indication that a restore succeeded. + +Instead: + +- append/replace the transaction state with an explicit terminal phase; +- retain enough witness material for restart reconciliation; +- cleanup of old transaction records is a separate bounded maintenance operation, not part of the critical promotion acknowledgement. + +### 7.2 Crash ambiguity rule + +After interruption, recovery inspects: + +- recovery transaction state; +- target root identity; +- staged candidate identity; +- displaced original identity; +- fingerprints; +- filesystem/volume support information. + +It then either: + +- deterministically resumes; +- deterministically rolls back; +- reports completed; +- or fails closed with all recoverable assets preserved. + +Absence of one directory entry is never sufficient evidence that no transaction existed. + +### 7.3 Windows supported-storage contract + +For v0.13 deterministic automatic restore, DKF SHALL positively identify a locally supported Windows persistence environment before destructive root mutation. + +Initial supported Windows automatic durability profile: + +- local Windows filesystem; +- NTFS; +- non-network path; +- native write-through/flush helper available and successfully probed. + +Other Windows storage profiles MUST fail closed **for the attempted destructive transition**, preserve recovery assets and provide operator recovery evidence. This is not a blanket disable of Windows restore APIs. + +Support may be broadened only with source-backed OS/filesystem evidence and tests. + +### 7.4 Required Issue #73 fixtures + +The final suite MUST include Windows-specific, non-skipped coverage equivalent to: + +- W73-01 recovery-intent publication boundary; +- W73-02 original displacement boundary; +- W73-03 candidate publication boundary; +- W73-04 promotion acknowledgement boundary; +- W73-05 displaced-original cleanup boundary; +- W73-06 interrupted rollback and repeat recovery; +- W73-07 invalid/tampered witness; +- W73-08 unsupported persistence profile fails safely before destructive mutation; +- W73-09 pre-existing interrupted restore remains recoverable; +- W73-10 normal full Windows restore succeeds and preserves specified metadata/integrity. + +Fault injection is evidence of ordering/error safety, not physical power-cut certification. + +## 8. Migration contract + +Migration MUST: + +- inventory supported legacy state; +- fingerprint source state before mutation; +- create a complete backup bundle; +- import canonical V2 entities/events; +- verify semantic equivalence against legacy source; +- create migration-complete witness only after verification; +- be idempotent; +- refuse changed/corrupt source; +- preserve legacy source until verified cutover; +- support explicit offline backup restoration. + +v0.12.1 feature state introduced after the original T04 design MUST be inventoried and classified before cutover. Evidence artifacts and disposable caches are not automatically promoted to canonical state. + +## 9. Locking and concurrency + +Within one local workspace: + +- one writer lock protects event sequencing and commit publication; +- stale locks may be reclaimed only when ownership is provably dead; +- a live local PID lock is never stolen due to age alone; +- multiple concurrent writers serialize deterministically. + +Distributed cross-host locking is not a v0.13 guarantee unless separately specified. + +Same-account hostile concurrent mutation remains the separate threat-model issue recorded as Issue #72 and is not silently folded into this acceptance. + +## 10. Security invariants + +- no symlink/junction/path traversal may redirect state or restore writes outside declared boundaries; +- internal canonical files must be ordinary files with expected link/inode properties; +- restore destination aliases/case collisions fail closed; +- unexpected filesystem objects fail closed; +- recovery never destroys the last authenticated recoverable copy; +- no PowerShell string interpolation of untrusted path data; +- native helper failure propagates as a typed failure; +- no silent fallback from a required Windows durability primitive to plain `fs.renameSync`. + +## 11. Required independent gates + +T04 is risk level 3. + +Required final gates: + +- specification verification; +- complete automated tests; +- Ubuntu exact-head CI; +- Windows exact-head CI; +- independent code review; +- independent architecture review; +- independent security review; +- security control-domain PASS; +- all in-scope P1/P2 review threads resolved; +- deterministic acceptance receipt bound to the exact reviewed source SHA. + +Test success alone is not acceptance. + +## 12. Implementation strategy from v0.12.1 + +The old v0.12 T04 branches/PRs are evidence sources only. + +v0.13 implementation MUST start from current `main` and selectively port/reconcile: + +- State Engine V2 canonical ledger/snapshot/index; +- legacy migration/backup/restore; +- Autopilot state-store adapter; +- orchestration-run state adapter; +- schemas and tests; +- Windows durability helper/protocol; +- review/acceptance tooling. + +It MUST preserve later v0.12.1 changes such as Gate Profiles and Verification Extension acceptance behavior. + +No force merge or wholesale replacement of current main files with old T04 versions is permitted. + +## 13. Release boundary + +Completion of this T04 increment authorizes only progression toward v0.13.0 release integration. + +It does not itself authorize: + +- publishing v0.13.0; +- deleting legacy state; +- dropping Windows support; +- closing Issue #73 before exact-source independent acceptance; +- adding unrelated v0.13 features. From 4ba6a8197f08f7ca6057f29613cc55e34e05bc58 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:28:18 +0200 Subject: [PATCH 002/177] feat(v0.13): port T04 candidate state-engine-v2.mjs --- runtime/orchestration/state-engine-v2.mjs | 1560 +++++++++++++++++++++ 1 file changed, 1560 insertions(+) create mode 100644 runtime/orchestration/state-engine-v2.mjs diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs new file mode 100644 index 000000000..b6fed6155 --- /dev/null +++ b/runtime/orchestration/state-engine-v2.mjs @@ -0,0 +1,1560 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +export const STATE_ENGINE_SCHEMA_VERSION = '2.0.0'; +export const STATE_EVENT_SCHEMA_VERSION = '1.0.0'; +export const STATE_SNAPSHOT_SCHEMA_VERSION = '2.0.0'; +export const STATE_INDEX_SCHEMA_VERSION = '1.0.0'; + +export const STATE_ENGINE_ROOT = '.development-kit/state'; +export const STATE_EVENTS_FILE = 'events.jsonl'; +export const STATE_SNAPSHOT_FILE = 'snapshot.json'; +export const STATE_INDEX_FILE = 'index.db'; +export const STATE_SCHEMA_FILE = 'schema.json'; + +const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/; +const EVENT_TYPE_PATTERN = /^[A-Z][A-Z0-9_]{1,95}$/; +const ENTITY_TYPE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +const MAX_ENTITY_ID_LENGTH = 512; +// Bounded blocking sleeps avoid starving a contended local state writer. +const STATE_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4)); +function waitStateLock(ms = 25) { Atomics.wait(STATE_LOCK_SLEEP, 0, 0, ms); } + +export class StateEngineError extends Error { + constructor(message, details = null) { + super(message); + this.name = 'StateEngineError'; + this.details = details; + } +} + +function plainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (!value || typeof value !== 'object') return value; + return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])); +} + +function stableJson(value) { + return JSON.stringify(stable(value)); +} + +function stablePretty(value) { + return `${JSON.stringify(stable(value), null, 2)}\n`; +} + +function sha256(value) { + return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`; +} + +function requireText(value, label) { + if (typeof value !== 'string' || !value.trim()) { + throw new StateEngineError(`${label} must be a non-empty string`); + } + return value.trim(); +} + +function optionalText(value, label) { + if (value === undefined || value === null) return null; + return requireText(value, label); +} + +function entityType(value) { + const normalized = requireText(value, 'entityType'); + if (!ENTITY_TYPE_PATTERN.test(normalized)) { + throw new StateEngineError('entityType contains unsupported characters'); + } + return normalized; +} + +function entityId(value) { + const normalized = requireText(value, 'entityId'); + if (normalized.length > MAX_ENTITY_ID_LENGTH || /[\r\n\0]/.test(normalized)) { + throw new StateEngineError('entityId is invalid'); + } + return normalized; +} + +function eventType(value) { + const normalized = requireText(value, 'eventType'); + if (!EVENT_TYPE_PATTERN.test(normalized)) { + throw new StateEngineError('eventType is invalid'); + } + return normalized; +} + +function timestamp(value, label = 'timestamp') { + if (typeof value !== 'string' || Number.isNaN(Date.parse(value))) { + throw new StateEngineError(`${label} must be a valid timestamp`); + } + return value; +} + +function stateRevision(value, { allowNull = false } = {}) { + if (allowNull && (value === undefined || value === null)) return null; + if (!Number.isInteger(value) || value < 1) { + throw new StateEngineError('stateRevision must be a positive integer'); + } + return value; +} + +function statePaths(rootDir = process.cwd()) { + const root = path.resolve(rootDir); + const stateRoot = path.resolve(root, STATE_ENGINE_ROOT); + const relative = path.relative(root, stateRoot); + if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { + throw new StateEngineError('State Engine path escapes project root'); + } + const paths = Object.freeze({ + root, + stateRoot, + events: path.join(stateRoot, STATE_EVENTS_FILE), + snapshot: path.join(stateRoot, STATE_SNAPSHOT_FILE), + index: path.join(stateRoot, STATE_INDEX_FILE), + schema: path.join(stateRoot, STATE_SCHEMA_FILE), + lock: path.join(stateRoot, 'state.lock'), + pending: path.join(stateRoot, 'pending-commit.json'), + }); + if (fs.existsSync(stateRoot)) { + assertStateRootRealpathSafe(paths); + assertStateFilesNotLinks(paths); + } + return paths; +} + +function assertStateRootRealpathSafe(paths) { + const rootReal = fs.realpathSync(paths.root); + const stateReal = fs.realpathSync(paths.stateRoot); + const relative = path.relative(rootReal, stateReal); + if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { + throw new StateEngineError('State Engine root resolves outside project root'); + } + return stateReal; +} + +function assertStateFilesNotLinks(paths) { + for (const field of ['events', 'snapshot', 'index', 'schema', 'pending', 'lock']) { + try { + const stat = fs.lstatSync(paths[field]); + if (stat.isSymbolicLink() || !stat.isFile()) { + throw new StateEngineError('State Engine internal file is not a regular file: ' + field); + } + if (stat.nlink > 1) { + throw new StateEngineError('State Engine internal file has multiple hard links: ' + field); + } + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + } +} + +/** + * Persist directory namespace updates before journal acknowledgement. + * POSIX uses the pinned directory FD; Windows must open a write-capable + * directory FD for fsync/FlushFileBuffers. Unavailable barriers fail closed. + */ +function flushStateDirectory(directory, pinnedDirFd = null) { + if (process.platform !== 'win32' && pinnedDirFd !== null) { + fs.fsyncSync(pinnedDirFd); + return; + } + let fd; + try { + fd = fs.openSync(directory, process.platform === 'win32' ? 'r+' : 'r'); + const stat = fs.fstatSync(fd); + if (!stat.isDirectory()) { + throw new StateEngineError('State Engine directory flush target is not a directory'); + } + if (pinnedDirFd !== null) { + const pinned = fs.fstatSync(pinnedDirFd); + if (!pinned.isDirectory() || pinned.dev !== stat.dev || pinned.ino !== stat.ino) { + throw new StateEngineError('State Engine directory changed before durability barrier'); + } + } + fs.fsyncSync(fd); + } catch (error) { + throw new StateEngineError('State Engine directory durability barrier failed; refusing acknowledgement', { + code: 'STATE_DIRECTORY_FLUSH_FAILED', + directory, + cause: error.code || error.message, + }); + } finally { + if (fd !== undefined) fs.closeSync(fd); + } +} + +function fsyncParentDirectory(directory) { + flushStateDirectory(directory); +} + +function pinnedUnlinkAndSync(filePath, pinnedDirFd = null) { + const directory = path.dirname(filePath); + const file = path.basename(filePath); + const ownsDirFd = !pinnedDirFd; + const dirFd = pinnedDirFd ?? fs.openSync(directory, 'r'); + try { + const pinned = fs.fstatSync(dirFd); + const observed = fs.lstatSync(directory); + if (!pinned.isDirectory() || !observed.isDirectory() || observed.isSymbolicLink() || + pinned.dev !== observed.dev || pinned.ino !== observed.ino) { + throw new StateEngineError('State Engine directory changed before journal cleanup'); + } + const rootDir = path.dirname(path.dirname(directory)); + assertStateRootRealpathSafe({ root: rootDir, stateRoot: directory }); + const target = process.platform === 'linux' ? path.join('/proc/self/fd/' + dirFd, file) : filePath; + const preStat = fs.lstatSync(target); + if (preStat.isSymbolicLink() || !preStat.isFile() || preStat.nlink > 1) { + throw new StateEngineError('Target journal file is not a regular unlinked file'); + } + // Trust model (T04, trusted/exclusive local runtime): two directory entries + // cannot be checked-and-unlinked atomically against a hostile same-UID + // process, so no ledger-binding check is attempted here. Ledger authority is + // enforced instead on every load, which rejects any snapshot/index that is + // inconsistent with canonical history (see loadStateSnapshot). + // Recheck directory immediately before unlink on platforms without descriptor-relative paths + const latest = fs.lstatSync(directory); + if (!latest.isDirectory() || latest.isSymbolicLink() || + latest.dev !== pinned.dev || latest.ino !== pinned.ino) { + throw new StateEngineError('State Engine directory was replaced before journal cleanup'); + } + fs.unlinkSync(target); + flushStateDirectory(directory, dirFd); + } finally { + if (ownsDirFd) fs.closeSync(dirFd); + } +} + +function atomicWrite(filePath, content, pinnedDirFd = null) { + const directory = path.dirname(filePath); + fs.mkdirSync(directory, { recursive: true }); + // Pin the already-validated State Engine directory before creating a + // journal, snapshot or index. Linux exposes descriptor-relative paths via + // /proc/self/fd; renames there cannot follow a subsequently swapped + // workspace directory symlink into an external target. + const ownsDirFd = !pinnedDirFd; + const dirFd = pinnedDirFd ?? fs.openSync(directory, 'r'); + const file = path.basename(filePath); + const pinnedDir = process.platform === 'linux' ? '/proc/self/fd/' + dirFd : directory; + const tempPath = path.join(pinnedDir, file + '.tmp-' + process.pid + '-' + crypto.randomUUID()); + const destination = path.join(pinnedDir, file); + let published = false; + try { + const pinned = fs.fstatSync(dirFd); + const observed = fs.lstatSync(directory); + if (!pinned.isDirectory() || !observed.isDirectory() || observed.isSymbolicLink() || + pinned.dev !== observed.dev || pinned.ino !== observed.ino) { + throw new StateEngineError('State Engine directory changed during durable write preparation'); + } + const rootDir = path.dirname(path.dirname(directory)); + assertStateRootRealpathSafe({ root: rootDir, stateRoot: directory }); + const fd = fs.openSync(tempPath, 'wx', 0o600); + try { + fs.writeFileSync(fd, content, 'utf8'); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + // On non-Linux platforms without handle-relative filesystem primitives, + // revalidate the directory identity immediately before publication. + // Hostile same-UID concurrent processes remain unsupported outside + // a trusted offline maintenance environment on those hosts. + const latest = fs.lstatSync(directory); + if (!latest.isDirectory() || latest.isSymbolicLink() || + latest.dev !== pinned.dev || latest.ino !== pinned.ino) { + throw new StateEngineError('State Engine directory was replaced before durable publication'); + } + fs.renameSync(tempPath, destination); + published = true; + flushStateDirectory(directory, dirFd); + return filePath; + } finally { + if (!published) { + try { fs.unlinkSync(tempPath); } catch {} + } + if (ownsDirFd) fs.closeSync(dirFd); + } +} + +function acquireStateLock(rootDir, timeoutMs = 12000) { + const paths = statePaths(rootDir); + fs.mkdirSync(paths.stateRoot, { recursive: true }); + assertStateRootRealpathSafe(paths); + const owner = 'state-lock-' + crypto.randomUUID(); + const started = Date.now(); + + while (Date.now() - started < timeoutMs) { + try { + const fd = fs.openSync(paths.lock, 'wx'); + try { + fs.writeFileSync(fd, JSON.stringify({ + owner, + pid: process.pid, + hostname: os.hostname(), + acquiredAt: new Date().toISOString(), + }), 'utf8'); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + return { path: paths.lock, owner }; + } catch (error) { + if (error.code !== 'EEXIST') { + // Windows can briefly deny create/open while another process closes or + // removes this exact lock file. Retry a bounded transient sharing + // violation; never interpret it as permission to steal the lock. + if (process.platform !== 'win32' || !['EPERM', 'EACCES'].includes(error.code)) { + throw new StateEngineError('Unable to acquire State Engine lock: ' + error.message); + } + waitStateLock(25); + continue; + } + let stat; + try { + stat = fs.statSync(paths.lock); + } catch (readError) { + if (readError.code === 'ENOENT') continue; + throw new StateEngineError('Cannot inspect existing State Engine lock: ' + readError.message); + } + if (Date.now() - stat.mtimeMs > 15_000) { + let prior; + try { + prior = JSON.parse(fs.readFileSync(paths.lock, 'utf8')); + } catch { + // Corrupt or incomplete lock metadata must never authorize lock stealing. + prior = null; + } + // A live local PID owns the lock regardless of its age. Remote and legacy + // owners are treated as unknown and cannot be stolen automatically. + if (prior && prior.hostname === os.hostname() && Number.isSafeInteger(prior.pid) && prior.pid > 0) { + let alive = true; + try { + process.kill(prior.pid, 0); + } catch (probeError) { + if (probeError.code === 'ESRCH') alive = false; + } + if (!alive) { + // Pathname compare-then-unlink is unsafe: another reclaimer may + // install a fresh live lock between the check and unlink. mkdir + // gives each local stale-lock reclaimer exclusive ownership of a + // recovery guard, so other reclaimers cannot race its unlink. + const reclaimGuard = paths.lock + '.reclaim'; + let guardOwned = false; + let guardMarker = null; + try { + try { + fs.mkdirSync(reclaimGuard, { mode: 0o700 }); + guardOwned = true; + guardMarker = path.join(reclaimGuard, 'owner.json'); + const guardFd = fs.openSync(guardMarker, 'wx', 0o600); + try { + fs.writeFileSync(guardFd, JSON.stringify({ + pid: process.pid, + hostname: os.hostname(), + owner, + acquiredAt: new Date().toISOString(), + })); + fs.fsyncSync(guardFd); + } finally { + fs.closeSync(guardFd); + } + } catch (guardError) { + if (guardError.code !== 'EEXIST') throw guardError; + } + if (guardOwned) { + let latest; + try { + latest = fs.statSync(paths.lock); + } catch (raceError) { + if (raceError.code !== 'ENOENT') throw raceError; + } + if (latest && latest.ino === stat.ino && latest.mtimeMs === stat.mtimeMs && latest.size === stat.size) { + let current; + try { + current = JSON.parse(fs.readFileSync(paths.lock, 'utf8')); + } catch (readError) { + if (readError.code !== 'ENOENT') throw readError; + } + if (current?.owner === prior.owner && current?.pid === prior.pid && current?.hostname === prior.hostname) { + // Verify ownership is still dead inside the reclaim guard. + let confirmedDead = false; + try { + process.kill(current.pid, 0); + } catch (probeError) { + confirmedDead = probeError.code === 'ESRCH'; + } + if (confirmedDead) { + try { + fs.unlinkSync(paths.lock); + } catch (removeError) { + if (removeError.code !== 'ENOENT') throw removeError; + } + } + } + } + } + } finally { + if (guardOwned) { + if (guardMarker && fs.existsSync(guardMarker)) fs.unlinkSync(guardMarker); + fs.rmdirSync(reclaimGuard); + } + } + if (guardOwned) continue; + } + } + } + // Use bounded, synchronous waiting so Node >=18 and current callers remain compatible. + waitStateLock(25); + } + } + throw new StateEngineError('State Engine lock acquisition timed out'); +} + +/** + * Explicit OFFLINE maintenance operation. Never reclaim an orphaned guard in + * the normal commit path: proving the reclaimer is gone requires inspecting + * owner metadata and ruling out an active writer. The caller must take the + * project out of service before acknowledging recovery. + */ +export function recoverAbandonedStateLock({ rootDir = process.cwd(), confirmOffline = false } = {}) { + if (confirmOffline !== true) { + throw new StateEngineError('Explicit offline confirmation is required for abandoned-lock recovery'); + } + const paths = statePaths(rootDir); + const guard = paths.lock + '.reclaim'; + if (!fs.existsSync(guard)) return Object.freeze({ recovered: false, reason: 'no guard' }); + const stat = fs.lstatSync(guard); + if (!stat.isDirectory() || stat.isSymbolicLink()) { + throw new StateEngineError('Abandoned lock guard is not a regular directory'); + } + if (Date.now() - stat.mtimeMs < 60_000) { + throw new StateEngineError('Refusing to reclaim a recently active State Engine guard'); + } + const marker = path.join(guard, 'owner.json'); + if (fs.existsSync(marker)) { + const owner = JSON.parse(fs.readFileSync(marker, 'utf8')); + if (owner.hostname !== os.hostname() || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) { + throw new StateEngineError('Cannot establish local guard ownership for offline recovery'); + } + try { + process.kill(owner.pid, 0); + throw new StateEngineError('Refusing to remove a live State Engine recovery guard'); + } catch (probe) { + if (probe.code !== 'ESRCH') throw probe; + } + } + if (fs.existsSync(paths.lock)) { + const lock = JSON.parse(fs.readFileSync(paths.lock, 'utf8')); + if (lock.hostname !== os.hostname() || !Number.isSafeInteger(lock.pid) || lock.pid <= 0) { + throw new StateEngineError('Offline guard recovery requires a provably local dead state lock'); + } + try { + process.kill(lock.pid, 0); + throw new StateEngineError('Refusing to recover a guard while state lock owner is alive'); + } catch (probe) { + if (probe.code !== 'ESRCH') throw probe; + } + } + if (fs.existsSync(marker)) fs.unlinkSync(marker); + fs.rmdirSync(guard); + return Object.freeze({ recovered: true, mode: 'confirmed-offline' }); +} + +function releaseStateLock(lock) { + if (!lock?.path) return; + try { + if (!fs.existsSync(lock.path)) return; + const current = JSON.parse(fs.readFileSync(lock.path, 'utf8')); + if (current.owner === lock.owner) fs.unlinkSync(lock.path); + } catch {} +} + +function schemaDocument() { + return Object.freeze({ + schemaVersion: STATE_ENGINE_SCHEMA_VERSION, + authority: { + canonicalHistory: STATE_EVENTS_FILE, + materializedSnapshot: STATE_SNAPSHOT_FILE, + disposableIndex: STATE_INDEX_FILE, + }, + contracts: { + event: STATE_EVENT_SCHEMA_VERSION, + snapshot: STATE_SNAPSHOT_SCHEMA_VERSION, + index: STATE_INDEX_SCHEMA_VERSION, + }, + invariants: [ + 'events.jsonl is canonical runtime state history', + 'snapshot.json is derived from events.jsonl', + 'index.db is disposable and rebuildable', + 'event sequence is monotonic', + 'event hashes form a single integrity chain', + ], + }); +} + +export function ensureStateEngineLayout(rootDir = process.cwd()) { + const paths = statePaths(rootDir); + fs.mkdirSync(paths.stateRoot, { recursive: true }); + assertStateRootRealpathSafe(paths); + + const expectedSchema = stablePretty(schemaDocument()); + if (fs.existsSync(paths.schema)) { + const existing = fs.readFileSync(paths.schema, 'utf8'); + if (existing !== expectedSchema) { + throw new StateEngineError('State Engine schema.json does not match the runtime schema contract'); + } + } else { + atomicWrite(paths.schema, expectedSchema); + } + + if (!fs.existsSync(paths.events)) { + if (fs.existsSync(paths.pending)) { + throw new StateEngineError('State Engine events.jsonl is missing while pending-commit.json is present; state integrity compromised'); + } + atomicWrite(paths.events, ''); + } + if (!fs.existsSync(paths.snapshot)) atomicWrite(paths.snapshot, stablePretty(emptySnapshot())); + if (!fs.existsSync(paths.index)) atomicWrite(paths.index, stablePretty(buildStateIndex(emptySnapshot()))); + + return paths; +} + +export function isStateEngineV2Active(rootDir = process.cwd()) { + const paths = statePaths(rootDir); + if (!fs.existsSync(paths.stateRoot)) return false; + + const schemaExists = fs.existsSync(paths.schema); + const eventsExist = fs.existsSync(paths.events); + if (!schemaExists && !eventsExist) return false; + if (!schemaExists || !eventsExist) { + throw new StateEngineError('State Engine V2 layout is incomplete'); + } + + const expectedSchema = stablePretty(schemaDocument()); + const actualSchema = fs.readFileSync(paths.schema, 'utf8'); + if (actualSchema !== expectedSchema) { + throw new StateEngineError('State Engine schema.json does not match the runtime schema contract'); + } + return true; +} + +function emptySnapshot() { + return { + schemaVersion: STATE_SNAPSHOT_SCHEMA_VERSION, + engineSchemaVersion: STATE_ENGINE_SCHEMA_VERSION, + lastEventSequence: 0, + lastEventHash: null, + entities: {}, + migrations: [], + }; +} + +function eventHashPayload(event) { + const { eventHash, ...payload } = event; + return payload; +} + +function computeEventId(base) { + const digest = crypto.createHash('sha256').update(stableJson(base)).digest('hex').slice(0, 20); + return `evt-${String(base.sequence).padStart(10, '0')}-${digest}`; +} + +function computeEventHash(event) { + return sha256(stableJson(eventHashPayload(event))); +} + +export function validateStateEvent(event, previous = null) { + if (!plainObject(event)) throw new StateEngineError('State event must be an object'); + if (event.schemaVersion !== STATE_EVENT_SCHEMA_VERSION) { + throw new StateEngineError(`Unsupported event schemaVersion: ${event.schemaVersion}`); + } + if (!Number.isInteger(event.sequence) || event.sequence < 1) throw new StateEngineError('Event sequence must be a positive integer'); + if (typeof event.eventId !== 'string' || !/^evt-\d{10}-[a-f0-9]{20}$/.test(event.eventId)) { + throw new StateEngineError('Event eventId is invalid'); + } + timestamp(event.timestamp, 'event.timestamp'); + requireText(event.actorClass, 'event.actorClass'); + eventType(event.eventType); + + if (event.entityType !== null) entityType(event.entityType); + if (event.entityId !== null) entityId(event.entityId); + for (const field of ['lifecycleId', 'taskId', 'contractId', 'runId']) optionalText(event[field], field); + if (!plainObject(event.payload)) throw new StateEngineError('Event payload must be an object'); + + if (previous === null) { + if (event.sequence !== 1) throw new StateEngineError('First state event sequence must be 1'); + if (event.previousEventHash !== null) throw new StateEngineError('First state event previousEventHash must be null'); + } else { + if (event.sequence !== previous.sequence + 1) throw new StateEngineError('Event sequence is not contiguous'); + if (event.previousEventHash !== previous.eventHash) throw new StateEngineError('Event previousEventHash does not match chain'); + } + + if (typeof event.eventHash !== 'string' || !SHA256_PATTERN.test(event.eventHash)) { + throw new StateEngineError('Event eventHash is invalid'); + } + if (event.eventHash !== computeEventHash(event)) { + throw new StateEngineError(`Event hash mismatch at sequence ${event.sequence}`); + } + + const base = { ...eventHashPayload(event) }; + delete base.eventId; + if (event.eventId !== computeEventId(base)) { + throw new StateEngineError(`Event ID mismatch at sequence ${event.sequence}`); + } + + return true; +} + +function parseLedger(content) { + if (!content) return []; + const lines = content.split(/\r?\n/); + if (lines.at(-1) === '') lines.pop(); + const events = []; + let previous = null; + + for (let i = 0; i < lines.length; i += 1) { + if (!lines[i].trim()) throw new StateEngineError(`Canonical state ledger contains a blank record at line ${i + 1}`); + let event; + try { + event = JSON.parse(lines[i]); + } catch (error) { + throw new StateEngineError(`Canonical state ledger JSON is invalid at line ${i + 1}: ${error.message}`); + } + validateStateEvent(event, previous); + events.push(event); + previous = event; + } + return events; +} + +export function loadCanonicalEvents(rootDir = process.cwd()) { + const paths = statePaths(rootDir); + if (!fs.existsSync(paths.events)) { + if (fs.existsSync(paths.pending)) { + throw new StateEngineError('State Engine events.jsonl is missing while pending-commit.json is present; state integrity compromised'); + } + return []; + } + if (fs.existsSync(paths.pending)) { + return withStateLock(rootDir, () => parseLedger(fs.readFileSync(paths.events, 'utf8'))); + } + const content = fs.readFileSync(paths.events, 'utf8'); + if (fs.existsSync(paths.pending)) { + return withStateLock(rootDir, () => parseLedger(fs.readFileSync(paths.events, 'utf8'))); + } + return parseLedger(content); +} + +function normalizePathSegments(value) { + if (!Array.isArray(value) || value.some((segment) => typeof segment !== 'string' || !segment || /[\r\n\0]/.test(segment))) { + throw new StateEngineError('Patch operation path must be a non-empty string array'); + } + return value; +} + +function cloneSerializable(value, label) { + try { + const cloned = structuredClone(value); + JSON.stringify(cloned); + return cloned; + } catch { + throw new StateEngineError(`${label} must be JSON-serializable`); + } +} + +function diffValues(before, after, pathSegments = []) { + if (stableJson(before) === stableJson(after)) return []; + + if (plainObject(before) && plainObject(after)) { + const operations = []; + const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort(); + for (const key of keys) { + const childPath = [...pathSegments, key]; + if (!(key in after)) { + operations.push({ op: 'delete', path: childPath }); + } else if (!(key in before)) { + operations.push({ op: 'set', path: childPath, value: cloneSerializable(after[key], 'Patch value') }); + } else { + operations.push(...diffValues(before[key], after[key], childPath)); + } + } + return operations; + } + + return [{ op: 'set', path: pathSegments, value: cloneSerializable(after, 'Patch value') }]; +} + +function setAtPath(target, segments, value) { + if (segments.length === 0) return cloneSerializable(value, 'Patch root value'); + let cursor = target; + for (let i = 0; i < segments.length - 1; i += 1) { + const key = segments[i]; + if (!plainObject(cursor[key])) cursor[key] = {}; + cursor = cursor[key]; + } + cursor[segments.at(-1)] = cloneSerializable(value, 'Patch value'); + return target; +} + +function deleteAtPath(target, segments) { + if (segments.length === 0) throw new StateEngineError('Cannot delete the entity-state root'); + let cursor = target; + for (let i = 0; i < segments.length - 1; i += 1) { + const key = segments[i]; + if (!plainObject(cursor[key])) return target; + cursor = cursor[key]; + } + delete cursor[segments.at(-1)]; + return target; +} + +function applyOperations(previousState, operations) { + if (!Array.isArray(operations)) throw new StateEngineError('State event operations must be an array'); + let next = cloneSerializable(previousState, 'Previous state'); + for (const operation of operations) { + if (!plainObject(operation)) throw new StateEngineError('Patch operation must be an object'); + const segments = normalizePathSegments(operation.path); + if (operation.op === 'set') { + next = setAtPath(next, segments, operation.value); + } else if (operation.op === 'delete') { + next = deleteAtPath(next, segments); + } else { + throw new StateEngineError(`Unsupported patch operation: ${operation.op}`); + } + } + return next; +} + +function ensureEntityBucket(snapshot, type) { + if (!snapshot.entities[type]) snapshot.entities[type] = {}; + return snapshot.entities[type]; +} + +export function rebuildSnapshotFromEvents(events) { + if (!Array.isArray(events)) throw new StateEngineError('events must be an array'); + const snapshot = emptySnapshot(); + let previous = null; + + for (const event of events) { + validateStateEvent(event, previous); + + if (event.eventType === 'ENTITY_STATE_INITIALIZED') { + if (event.entityType === null || event.entityId === null) throw new StateEngineError('State initialization event requires entity identity'); + const bucket = ensureEntityBucket(snapshot, event.entityType); + if (bucket[event.entityId]) throw new StateEngineError(`Entity is initialized more than once: ${event.entityType}/${event.entityId}`); + const state = cloneSerializable(event.payload.state, 'Initialized state'); + const revision = stateRevision(event.payload.stateRevision); + bucket[event.entityId] = { + stateRevision: revision, + state, + lastEventSequence: event.sequence, + lastEventHash: event.eventHash, + refs: { + lifecycleId: event.lifecycleId, + taskId: event.taskId, + contractId: event.contractId, + runId: event.runId, + }, + }; + } else if (event.eventType === 'ENTITY_STATE_UPDATED') { + if (event.entityType === null || event.entityId === null) throw new StateEngineError('State update event requires entity identity'); + const bucket = ensureEntityBucket(snapshot, event.entityType); + const current = bucket[event.entityId]; + if (!current) throw new StateEngineError(`State update references unknown entity: ${event.entityType}/${event.entityId}`); + const revision = stateRevision(event.payload.stateRevision); + if (revision < current.stateRevision) { + throw new StateEngineError(`Entity stateRevision regressed for ${event.entityType}/${event.entityId}`); + } + current.state = applyOperations(current.state, event.payload.operations); + current.stateRevision = revision; + current.lastEventSequence = event.sequence; + current.lastEventHash = event.eventHash; + current.refs = { + lifecycleId: event.lifecycleId ?? current.refs.lifecycleId, + taskId: event.taskId ?? current.refs.taskId, + contractId: event.contractId ?? current.refs.contractId, + runId: event.runId ?? current.refs.runId, + }; + } else if (event.eventType === 'MIGRATION_COMPLETED') { + const migrationId = requireText(event.payload.migrationId, 'migrationId'); + const sourceFingerprint = requireText(event.payload.sourceFingerprint, 'sourceFingerprint'); + if (!SHA256_PATTERN.test(sourceFingerprint)) throw new StateEngineError('Migration sourceFingerprint is invalid'); + if (!snapshot.migrations.some((migration) => migration.migrationId === migrationId)) { + snapshot.migrations.push({ + migrationId, + sourceFingerprint, + completedAt: event.timestamp, + entityCount: Number(event.payload.entityCount ?? 0), + semanticEquivalence: event.payload.semanticEquivalence === true, + }); + } + } + + snapshot.lastEventSequence = event.sequence; + snapshot.lastEventHash = event.eventHash; + previous = event; + } + + snapshot.migrations.sort((a, b) => a.migrationId.localeCompare(b.migrationId)); + return snapshot; +} + +export function validateStateSnapshot(snapshot) { + if (!plainObject(snapshot)) throw new StateEngineError('State snapshot must be an object'); + if (snapshot.schemaVersion !== STATE_SNAPSHOT_SCHEMA_VERSION) throw new StateEngineError('Unsupported snapshot schemaVersion'); + if (snapshot.engineSchemaVersion !== STATE_ENGINE_SCHEMA_VERSION) throw new StateEngineError('Snapshot engine schema mismatch'); + if (!Number.isInteger(snapshot.lastEventSequence) || snapshot.lastEventSequence < 0) throw new StateEngineError('Snapshot lastEventSequence is invalid'); + if (snapshot.lastEventHash !== null && !SHA256_PATTERN.test(snapshot.lastEventHash)) throw new StateEngineError('Snapshot lastEventHash is invalid'); + if (!plainObject(snapshot.entities)) throw new StateEngineError('Snapshot entities must be an object'); + if (!Array.isArray(snapshot.migrations)) throw new StateEngineError('Snapshot migrations must be an array'); + + for (const [type, bucket] of Object.entries(snapshot.entities)) { + entityType(type); + if (!plainObject(bucket)) throw new StateEngineError(`Snapshot entity bucket is invalid: ${type}`); + for (const [id, record] of Object.entries(bucket)) { + entityId(id); + if (!plainObject(record)) throw new StateEngineError('Snapshot entity record must be an object'); + stateRevision(record.stateRevision); + if (!Number.isInteger(record.lastEventSequence) || record.lastEventSequence < 1) throw new StateEngineError('Entity lastEventSequence is invalid'); + if (!SHA256_PATTERN.test(record.lastEventHash)) throw new StateEngineError('Entity lastEventHash is invalid'); + cloneSerializable(record.state, 'Entity state'); + } + } + return true; +} + +function indexArrayPush(map, key, value) { + if (!key) return; + if (!map[key]) map[key] = []; + if (!map[key].includes(value)) map[key].push(value); +} + +export function buildStateIndex(snapshot) { + validateStateSnapshot(snapshot); + const records = []; + const byContract = {}; + const byTask = {}; + const byRun = {}; + const byLifecycle = {}; + + for (const [type, bucket] of Object.entries(snapshot.entities).sort(([a], [b]) => a.localeCompare(b))) { + for (const [id, record] of Object.entries(bucket).sort(([a], [b]) => a.localeCompare(b))) { + const key = `${type}::${id}`; + records.push({ + key, + entityType: type, + entityId: id, + stateRevision: record.stateRevision, + lastEventSequence: record.lastEventSequence, + lastEventHash: record.lastEventHash, + }); + indexArrayPush(byContract, record.refs?.contractId, key); + indexArrayPush(byTask, record.refs?.taskId, key); + indexArrayPush(byRun, record.refs?.runId, key); + indexArrayPush(byLifecycle, record.refs?.lifecycleId, key); + } + } + + for (const map of [byContract, byTask, byRun, byLifecycle]) { + for (const values of Object.values(map)) values.sort(); + } + + return { + schemaVersion: STATE_INDEX_SCHEMA_VERSION, + sourceSequence: snapshot.lastEventSequence, + sourceHash: snapshot.lastEventHash, + recordCount: records.length, + records, + byContract, + byTask, + byRun, + byLifecycle, + }; +} + +export function validateStateIndex(index, snapshot = null) { + if (!plainObject(index)) throw new StateEngineError('State index must be an object'); + if (index.schemaVersion !== STATE_INDEX_SCHEMA_VERSION) throw new StateEngineError('Unsupported index schemaVersion'); + if (!Number.isInteger(index.sourceSequence) || index.sourceSequence < 0) throw new StateEngineError('Index sourceSequence is invalid'); + if (index.sourceHash !== null && !SHA256_PATTERN.test(index.sourceHash)) throw new StateEngineError('Index sourceHash is invalid'); + if (!Array.isArray(index.records)) throw new StateEngineError('Index records must be an array'); + if (index.recordCount !== index.records.length) throw new StateEngineError('Index recordCount mismatch'); + for (const field of ['byContract', 'byTask', 'byRun', 'byLifecycle']) { + if (!plainObject(index[field])) throw new StateEngineError(`Index ${field} must be an object`); + } + if (snapshot) { + validateStateSnapshot(snapshot); + if (index.sourceSequence !== snapshot.lastEventSequence || index.sourceHash !== snapshot.lastEventHash) { + throw new StateEngineError('State index is stale relative to snapshot'); + } + } + return true; +} + +function persistDerived(snapshot, rootDir, pinnedDirFd = null) { + validateStateSnapshot(snapshot); + const paths = pinnedDirFd ? statePaths(rootDir) : ensureStateEngineLayout(rootDir); + const index = buildStateIndex(snapshot); + atomicWrite(paths.snapshot, stablePretty(snapshot), pinnedDirFd); + atomicWrite(paths.index, stablePretty(index), pinnedDirFd); + return { snapshot, index }; +} + +function repairDerivedLocked(rootDir) { + // Snapshot repair replays the *current* ledger under the writer lock, never a + // stale cached history from before another writer committed a later event. + const paths = ensureStateEngineLayout(rootDir); + const canonical = rebuildSnapshotFromEvents(parseLedger(fs.readFileSync(paths.events, 'utf8'))); + if (fs.existsSync(paths.snapshot)) { + let witness = null; + try { + witness = JSON.parse(fs.readFileSync(paths.snapshot, 'utf8')); + validateStateSnapshot(witness); + } catch { + witness = null; + } + if (witness && witness.lastEventSequence > canonical.lastEventSequence) { + throw new StateEngineError('Canonical state history appears truncated relative to the materialized snapshot'); + } + if (witness && witness.lastEventSequence === canonical.lastEventSequence + && witness.lastEventHash !== canonical.lastEventHash) { + throw new StateEngineError('Canonical state history hash differs from the materialized snapshot integrity witness'); + } + } + persistDerived(canonical, rootDir); + return canonical; +} + +export function rebuildStateSnapshot(rootDir = process.cwd()) { + return withStateLock(rootDir, () => repairDerivedLocked(rootDir)); +} + +export function loadStateSnapshot(rootDir = process.cwd(), { rebuildIfNeeded = true } = {}) { + const paths = statePaths(rootDir); + if (!fs.existsSync(paths.events)) { + if (fs.existsSync(paths.pending)) { + throw new StateEngineError('State Engine events.jsonl is missing while pending-commit.json is present; state integrity compromised'); + } + return null; + } + const canonical = rebuildSnapshotFromEvents(loadCanonicalEvents(rootDir)); + + if (!fs.existsSync(paths.snapshot)) { + if (!rebuildIfNeeded) return null; + return withStateLock(rootDir, () => repairDerivedLocked(rootDir)); + } + + let current; + try { + current = JSON.parse(fs.readFileSync(paths.snapshot, 'utf8')); + validateStateSnapshot(current); + } catch (error) { + if (!rebuildIfNeeded) throw error; + return withStateLock(rootDir, () => repairDerivedLocked(rootDir)); + } + + if (current.lastEventSequence > canonical.lastEventSequence) { + // A legitimate writer can advance the snapshot between a reader's ledger + // and snapshot reads. Check the latest canonical history before declaring + // truncation, without silencing a genuine witnessed history regression. + if (rebuildIfNeeded) { + const refreshed = rebuildSnapshotFromEvents(loadCanonicalEvents(rootDir)); + if (refreshed.lastEventSequence > current.lastEventSequence + || (refreshed.lastEventSequence === current.lastEventSequence + && refreshed.lastEventHash === current.lastEventHash)) { + return refreshed; + } + } + throw new StateEngineError( + 'Canonical state history appears truncated relative to the materialized snapshot', + { + snapshotSequence: current.lastEventSequence, + canonicalSequence: canonical.lastEventSequence, + snapshotHash: current.lastEventHash, + canonicalHash: canonical.lastEventHash, + }, + ); + } + if (current.lastEventSequence === canonical.lastEventSequence + && current.lastEventHash !== canonical.lastEventHash) { + throw new StateEngineError( + 'Canonical state history hash differs from the materialized snapshot integrity witness', + { + sequence: current.lastEventSequence, + snapshotHash: current.lastEventHash, + canonicalHash: canonical.lastEventHash, + }, + ); + } + if (stableJson(current) !== stableJson(canonical)) { + if (!rebuildIfNeeded) throw new StateEngineError('Materialized snapshot does not match canonical history'); + return withStateLock(rootDir, () => repairDerivedLocked(rootDir)); + } + return current; +} + +export function rebuildStateIndex(rootDir = process.cwd()) { + return withStateLock(rootDir, () => { + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }) ?? emptySnapshot(); + const paths = ensureStateEngineLayout(rootDir); + const index = buildStateIndex(snapshot); + atomicWrite(paths.index, stablePretty(index)); + return index; + }); +} + +export function loadStateIndex(rootDir = process.cwd(), { rebuildIfNeeded = true } = {}) { + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }) ?? emptySnapshot(); + const paths = statePaths(rootDir); + if (!fs.existsSync(paths.index)) { + return rebuildIfNeeded ? rebuildStateIndex(rootDir) : null; + } + try { + const index = JSON.parse(fs.readFileSync(paths.index, 'utf8')); + validateStateIndex(index, snapshot); + return index; + } catch (error) { + if (!rebuildIfNeeded) throw error; + return rebuildStateIndex(rootDir); + } +} + +function buildEvent({ + sequence, + previousEventHash, + timestamp: at, + actorClass, + type, + id: entityIdentifier = null, + entity: entityKind = null, + refs = {}, + payload, +}) { + const base = { + schemaVersion: STATE_EVENT_SCHEMA_VERSION, + sequence, + timestamp: timestamp(at, 'event timestamp'), + actorClass: requireText(actorClass, 'actorClass'), + eventType: eventType(type), + entityType: entityKind === null ? null : entityType(entityKind), + entityId: entityIdentifier === null ? null : entityId(entityIdentifier), + lifecycleId: optionalText(refs.lifecycleId, 'lifecycleId'), + taskId: optionalText(refs.taskId, 'taskId'), + contractId: optionalText(refs.contractId, 'contractId'), + runId: optionalText(refs.runId, 'runId'), + payload: cloneSerializable(payload, 'Event payload'), + previousEventHash: previousEventHash ?? null, + }; + const eventId = computeEventId(base); + const event = { + ...base, + eventId, + }; + event.eventHash = computeEventHash(event); + return event; +} + +function appendDurably(filePath, appendedText, expectedIdentity = null, pinnedDirFd = null, heldLedgerFd = null) { + const directory = path.dirname(filePath); + const file = path.basename(filePath); + const ownsDirFd = !pinnedDirFd; + const dirFd = pinnedDirFd ?? fs.openSync(directory, 'r'); + try { + const pinned = fs.fstatSync(dirFd); + const observed = fs.lstatSync(directory); + if (!pinned.isDirectory() || !observed.isDirectory() || observed.isSymbolicLink() || + pinned.dev !== observed.dev || pinned.ino !== observed.ino) { + throw new StateEngineError('State Engine directory changed before durable append'); + } + const rootDir = path.dirname(path.dirname(directory)); + assertStateRootRealpathSafe({ root: rootDir, stateRoot: directory }); + const target = process.platform === 'linux' ? path.join('/proc/self/fd/' + dirFd, file) : filePath; + // O_NOFOLLOW rejects a final-component symlink on supported POSIX hosts. + // On Windows, verify the opened inode and current directory entry before + // writing because path-only preflight cannot prevent a replacement race. + const flags = fs.constants.O_WRONLY | fs.constants.O_APPEND | (fs.constants.O_NOFOLLOW ?? 0); + const fd = fs.openSync(target, flags); + try { + const stat = fs.fstatSync(fd); + const entry = fs.lstatSync(target); + if (!stat.isFile() || stat.nlink !== 1 || entry.isSymbolicLink() || + !entry.isFile() || entry.nlink !== 1 || + entry.dev !== stat.dev || entry.ino !== stat.ino) { + throw new StateEngineError('Canonical event file symlink/inode mismatch or multiple hard links'); + } + if (heldLedgerFd !== null) { + const heldStat = fs.fstatSync(heldLedgerFd); + if (heldStat.dev !== stat.dev || heldStat.ino !== stat.ino) { + throw new StateEngineError('Canonical ledger inode changed while holding preparation descriptor'); + } + } + if (expectedIdentity && (stat.dev !== expectedIdentity.dev || + stat.ino !== expectedIdentity.ino || stat.size !== expectedIdentity.size)) { + throw new StateEngineError('Canonical event inode changed between event preparation and append'); + } + // Recheck directory immediately before write on platforms without descriptor-relative paths + const latest = fs.lstatSync(directory); + if (!latest.isDirectory() || latest.isSymbolicLink() || + latest.dev !== pinned.dev || latest.ino !== pinned.ino) { + throw new StateEngineError('State Engine directory was replaced before durable append'); + } + const bytes = Buffer.from(appendedText, 'utf8'); + let written = 0; + while (written < bytes.length) { + const amount = fs.writeSync(fd, bytes, written, bytes.length - written); + if (amount <= 0) throw new StateEngineError('Canonical event append made no forward progress'); + written += amount; + } + fs.fsyncSync(fd); + // Re-verify the directory entry after fsync to guarantee the canonical + // path still references this exact inode and was not unlinked or swapped + // during write (PRRC_kwDOTd-YNM79WYGR). + const finalEntry = fs.lstatSync(target); + const finalStat = fs.fstatSync(fd); + if (!finalEntry.isFile() || finalEntry.isSymbolicLink() || finalEntry.nlink !== 1 || + finalEntry.dev !== finalStat.dev || finalEntry.ino !== finalStat.ino) { + throw new StateEngineError('Canonical ledger directory entry was displaced during durable append'); + } + } finally { + fs.closeSync(fd); + } + flushStateDirectory(directory, dirFd); + } finally { + if (ownsDirFd) fs.closeSync(dirFd); + } +} + +function recoverPendingCommitLocked(rootDir) { + const paths = ensureStateEngineLayout(rootDir); + const dirFd = fs.openSync(paths.stateRoot, 'r'); + try { + const pinned = fs.fstatSync(dirFd); + const observed = fs.lstatSync(paths.stateRoot); + if (!pinned.isDirectory() || !observed.isDirectory() || observed.isSymbolicLink() || + pinned.dev !== observed.dev || pinned.ino !== observed.ino) { + throw new StateEngineError('State Engine directory changed before recovery preparation'); + } + const pendingTarget = process.platform === 'linux' + ? path.join('/proc/self/fd/' + dirFd, path.basename(paths.pending)) + : paths.pending; + const eventsTarget = process.platform === 'linux' + ? path.join('/proc/self/fd/' + dirFd, path.basename(paths.events)) + : paths.events; + const snapshotTarget = process.platform === 'linux' + ? path.join('/proc/self/fd/' + dirFd, path.basename(paths.snapshot)) + : paths.snapshot; + + if (!fs.existsSync(pendingTarget)) return; + if (!fs.existsSync(eventsTarget)) { + throw new StateEngineError('Cannot recover pending commit: canonical ledger events.jsonl is missing'); + } + let pending; + try { + pending = JSON.parse(fs.readFileSync(pendingTarget, 'utf8')); + } catch (error) { + throw new StateEngineError('Pending State Engine commit is invalid: ' + error.message); + } + if ( + pending.schemaVersion !== '1.0.0' + || !Number.isSafeInteger(pending.previousByteLength) + || pending.previousByteLength < 0 + || !SHA256_PATTERN.test(pending.previousHash) + || typeof pending.suffix !== 'string' + || !pending.suffix.endsWith('\n') + ) { + throw new StateEngineError('Pending State Engine commit metadata is invalid'); + } + // Bind every recovery operation to the same verified canonical descriptor. + // Pathname truncation can otherwise modify a hardlinked external file when + // an attacker replaces events.jsonl after the prefix has been inspected. + const flags = fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW ?? 0); + const fd = fs.openSync(eventsTarget, flags); + let canonical; + let canonicalStat; + try { + const opened = fs.fstatSync(fd); + canonicalStat = opened; + const entry = fs.lstatSync(eventsTarget); + if (!opened.isFile() || !entry.isFile() || entry.isSymbolicLink() || + opened.nlink !== 1 || entry.nlink !== 1 || + opened.dev !== entry.dev || opened.ino !== entry.ino) { + throw new StateEngineError('Recovery canonical ledger inode/symlink/hardlink integrity failed'); + } + const bytes = Buffer.alloc(opened.size); + let read = 0; + while (read < bytes.length) { + const amount = fs.readSync(fd, bytes, read, bytes.length - read, read); + if (amount <= 0) throw new StateEngineError('Canonical ledger changed while recovering pending commit'); + read += amount; + } + if (bytes.length < pending.previousByteLength) { + throw new StateEngineError('Canonical state history was truncated during a pending commit'); + } + if (sha256(bytes.subarray(0, pending.previousByteLength)) !== pending.previousHash) { + throw new StateEngineError('Canonical state prefix differs from pending commit integrity witness'); + } + const existingSuffix = bytes.subarray(pending.previousByteLength); + const expectedSuffix = Buffer.from(pending.suffix, 'utf8'); + if (!expectedSuffix.subarray(0, existingSuffix.length).equals(existingSuffix)) { + throw new StateEngineError('Canonical state suffix conflicts with pending commit; refusing recovery'); + } + if (existingSuffix.length < expectedSuffix.length) { + // Truncate and repair through this FD, never a separately resolved path. + fs.ftruncateSync(fd, pending.previousByteLength); + let written = 0; + while (written < expectedSuffix.length) { + const amount = fs.writeSync(fd, expectedSuffix, written, + expectedSuffix.length - written, pending.previousByteLength + written); + if (amount <= 0) throw new StateEngineError('Pending recovery append made no forward progress'); + written += amount; + } + } + // Recovery can observe a fully written suffix that was never fsynced by + // the interrupted writer. Flush the canonical ledger unconditionally + // before publishing derived views or acknowledging the pending journal. + fs.fsyncSync(fd); + // Re-verify the directory entry after fsync to ensure the canonical path + // still names the repaired inode before publishing derived views (PRRC_kwDOTd-YNM79WYGR). + const finalEntry = fs.lstatSync(eventsTarget); + const finalStat = fs.fstatSync(fd); + if (!finalEntry.isFile() || finalEntry.isSymbolicLink() || finalEntry.nlink !== 1 || + finalEntry.dev !== finalStat.dev || finalEntry.ino !== finalStat.ino) { + throw new StateEngineError('Canonical ledger directory entry was displaced during pending recovery'); + } + canonical = rebuildSnapshotFromEvents( + parseLedger(Buffer.concat([bytes.subarray(0, pending.previousByteLength), expectedSuffix]).toString('utf8')), + ); + // Reconstruct both derived views before acknowledging the durable commit. + let witness = null; + try { + witness = JSON.parse(fs.readFileSync(snapshotTarget, 'utf8')); + validateStateSnapshot(witness); + } catch { + witness = null; + } + if (witness && witness.lastEventSequence > canonical.lastEventSequence) { + throw new StateEngineError('Cannot recover a pending commit over a later snapshot integrity witness'); + } + if (witness && witness.lastEventSequence === canonical.lastEventSequence + && witness.lastEventHash !== canonical.lastEventHash) { + throw new StateEngineError('Pending recovery conflicts with snapshot integrity witness'); + } + persistDerived(canonical, rootDir, dirFd); + pinnedUnlinkAndSync(paths.pending, dirFd); + } finally { + fs.closeSync(fd); + } + } finally { + fs.closeSync(dirFd); + } +} + +const ACTIVE_STATE_LOCKS = new Set(); +function withStateLock(rootDir, task) { + const key = path.resolve(rootDir); + if (ACTIVE_STATE_LOCKS.has(key)) return task(); + const lock = acquireStateLock(rootDir); + ACTIVE_STATE_LOCKS.add(key); + try { + recoverPendingCommitLocked(rootDir); + return task(); + } finally { + ACTIVE_STATE_LOCKS.delete(key); + releaseStateLock(lock); + } +} + +function commitPreparedEventsLocked(newEvents, rootDir = process.cwd()) { + if (!Array.isArray(newEvents) || newEvents.length === 0) { + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }) ?? emptySnapshot(); + return { events: [], snapshot, index: loadStateIndex(rootDir, { rebuildIfNeeded: true }) }; + } + + const paths = ensureStateEngineLayout(rootDir); + const dirFd = fs.openSync(paths.stateRoot, 'r'); + try { + const pinned = fs.fstatSync(dirFd); + const observed = fs.lstatSync(paths.stateRoot); + if (!pinned.isDirectory() || !observed.isDirectory() || observed.isSymbolicLink() || + pinned.dev !== observed.dev || pinned.ino !== observed.ino) { + throw new StateEngineError('State Engine directory changed before durable write preparation'); + } + const rootDirParent = path.dirname(path.dirname(paths.stateRoot)); + assertStateRootRealpathSafe({ root: rootDirParent, stateRoot: paths.stateRoot }); + + const ledgerPath = process.platform === 'linux' + ? path.join('/proc/self/fd/' + dirFd, path.basename(paths.events)) + : paths.events; + + // Open ledger with descriptor-bound flags and read through this descriptor + // so preparation and identity witness come from the exact same descriptor. + // Retain this descriptor open across atomicWrite and appendDurably so the inode + // cannot be unlinked and recycled while the commit is in flight (PRRT_kwDOTd-YNM6ri6bH). + const readFlags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0); + const readFd = fs.openSync(ledgerPath, readFlags); + let existingContent; + let expectedIdentity; + try { + const openedStat = fs.fstatSync(readFd); + const entryStat = fs.lstatSync(ledgerPath); + if (!openedStat.isFile() || openedStat.nlink !== 1 || entryStat.isSymbolicLink() || + !entryStat.isFile() || entryStat.nlink !== 1 || + openedStat.dev !== entryStat.dev || openedStat.ino !== entryStat.ino) { + throw new StateEngineError('Canonical event file symlink/inode mismatch or multiple hard links'); + } + expectedIdentity = { dev: openedStat.dev, ino: openedStat.ino, size: openedStat.size }; + const bytes = Buffer.alloc(openedStat.size); + let read = 0; + while (read < bytes.length) { + const amount = fs.readSync(readFd, bytes, read, bytes.length - read, read); + if (amount <= 0 && read < bytes.length) { + throw new StateEngineError('Canonical ledger changed while reading during preparation'); + } + if (amount <= 0) break; + read += amount; + } + existingContent = bytes.toString('utf8'); + + const existing = parseLedger(existingContent); + let previous = existing.at(-1) ?? null; + for (const event of newEvents) { + validateStateEvent(event, previous); + previous = event; + } + const allEvents = [...existing, ...newEvents]; + // Validate and reconstruct before beginning a durable state mutation. + const nextSnapshot = rebuildSnapshotFromEvents(allEvents); + // A valid last event may lack the optional terminating newline. Preserve + // record separation when appending, including in the crash-recovery journal. + const separator = existingContent.length > 0 && !existingContent.endsWith('\n') ? '\n' : ''; + const suffix = separator + newEvents.map((event) => JSON.stringify(stable(event))).join('\n') + '\n'; + const pending = { + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(existingContent, 'utf8'), + previousHash: sha256(Buffer.from(existingContent, 'utf8')), + suffix, + }; + + atomicWrite(paths.pending, stablePretty(pending), dirFd); + // Physical append avoids O(history) rewrites; the pending journal permits + // deterministic repair after a torn write or interrupted snapshot persist. + appendDurably(paths.events, suffix, expectedIdentity, dirFd, readFd); + const { index } = persistDerived(nextSnapshot, rootDir, dirFd); + pinnedUnlinkAndSync(paths.pending, dirFd); + return { events: newEvents, snapshot: nextSnapshot, index }; + } finally { + fs.closeSync(readFd); + } + } finally { + fs.closeSync(dirFd); + } +} + +function nextEventContext(rootDir) { + const events = loadCanonicalEvents(rootDir); + const previous = events.at(-1) ?? null; + return { + sequence: previous ? previous.sequence + 1 : 1, + previousEventHash: previous?.eventHash ?? null, + previous, + }; +} + +export function appendEntityState({ + rootDir = process.cwd(), + entityType: type, + entityId: id, + state, + refs = {}, + actorClass = 'runtime', + timestamp: at = new Date().toISOString(), +} = {}) { + const normalizedType = entityType(type); + const normalizedId = entityId(id); + const nextState = cloneSerializable(state, 'Entity state'); + const revision = stateRevision(nextState.stateRevision); + + return withStateLock(rootDir, () => { + const currentSnapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }) ?? emptySnapshot(); + const current = currentSnapshot.entities[normalizedType]?.[normalizedId] ?? null; + + if (current && revision < current.stateRevision) { + throw new StateEngineError('Refusing stateRevision regression for ' + normalizedType + '/' + normalizedId); + } + if (current && stableJson(current.state) === stableJson(nextState)) { + return Object.freeze({ changed: false, event: null, snapshot: currentSnapshot }); + } + + const context = nextEventContext(rootDir); + const payload = current + ? { stateRevision: revision, operations: diffValues(current.state, nextState) } + : { stateRevision: revision, state: nextState }; + const event = buildEvent({ + sequence: context.sequence, + previousEventHash: context.previousEventHash, + timestamp: at, + actorClass, + type: current ? 'ENTITY_STATE_UPDATED' : 'ENTITY_STATE_INITIALIZED', + id: normalizedId, + entity: normalizedType, + refs, + payload, + }); + const committed = commitPreparedEventsLocked([event], rootDir); + return Object.freeze({ changed: true, event, snapshot: committed.snapshot }); + }); +} + +export function appendMetadataEvent({ + rootDir = process.cwd(), + eventType: type, + payload, + refs = {}, + actorClass = 'runtime', + timestamp: at = new Date().toISOString(), +} = {}) { + return withStateLock(rootDir, () => { + const context = nextEventContext(rootDir); + const event = buildEvent({ + sequence: context.sequence, + previousEventHash: context.previousEventHash, + timestamp: at, + actorClass, + type, + id: null, + entity: null, + refs, + payload, + }); + const committed = commitPreparedEventsLocked([event], rootDir); + return Object.freeze({ event, snapshot: committed.snapshot }); + }); +} + +export function loadEntityState(type, id, rootDir = process.cwd()) { + const normalizedType = entityType(type); + const normalizedId = entityId(id); + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); + const record = snapshot?.entities?.[normalizedType]?.[normalizedId]; + return record ? structuredClone(record.state) : null; +} + +export function listEntityRecords(type, rootDir = process.cwd()) { + const normalizedType = entityType(type); + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); + const bucket = snapshot?.entities?.[normalizedType] ?? {}; + return Object.entries(bucket) + .map(([id, record]) => ({ + entityId: id, + stateRevision: record.stateRevision, + lastEventSequence: record.lastEventSequence, + refs: structuredClone(record.refs), + })) + .sort((a, b) => a.entityId.localeCompare(b.entityId)); +} + +export function loadLatestEntityState(type, rootDir = process.cwd()) { + const normalizedType = entityType(type); + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); + const bucket = snapshot?.entities?.[normalizedType] ?? {}; + const records = Object.entries(bucket) + .map(([id, record]) => ({ id, record })) + .sort((a, b) => b.record.lastEventSequence - a.record.lastEventSequence || a.id.localeCompare(b.id)); + if (records.length === 0) return null; + return structuredClone(records[0].record.state); +} + +export function hasCompletedStateMigration(rootDir = process.cwd(), sourceFingerprint = null) { + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); + const migrations = snapshot?.migrations ?? []; + if (sourceFingerprint === null || sourceFingerprint === undefined) { + return migrations.some((migration) => migration.semanticEquivalence === true); + } + return migrations.some((migration) => ( + migration.semanticEquivalence === true + && migration.sourceFingerprint === sourceFingerprint + )); +} + +export function queryStateIndex({ + rootDir = process.cwd(), + contractId = null, + taskId = null, + runId = null, + lifecycleId = null, +} = {}) { + const index = loadStateIndex(rootDir, { rebuildIfNeeded: true }); + const filters = [ + ['byContract', contractId], + ['byTask', taskId], + ['byRun', runId], + ['byLifecycle', lifecycleId], + ].filter(([, value]) => value !== null && value !== undefined); + + if (filters.length === 0) return structuredClone(index.records); + + let keys = null; + for (const [field, value] of filters) { + const current = new Set(index[field][String(value)] ?? []); + keys = keys === null ? current : new Set([...keys].filter((key) => current.has(key))); + } + return index.records.filter((record) => keys.has(record.key)).map((record) => structuredClone(record)); +} + +export function verifyStateEngineIntegrity(rootDir = process.cwd()) { + const events = loadCanonicalEvents(rootDir); + const canonical = rebuildSnapshotFromEvents(events); + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: false }); + const index = loadStateIndex(rootDir, { rebuildIfNeeded: false }); + + if (snapshot === null) throw new StateEngineError('State snapshot is missing'); + if (stableJson(snapshot) !== stableJson(canonical)) { + throw new StateEngineError('State snapshot is not semantically equivalent to canonical history'); + } + validateStateIndex(index, snapshot); + const rebuiltIndex = buildStateIndex(snapshot); + if (stableJson(index) !== stableJson(rebuiltIndex)) { + throw new StateEngineError('State index is not semantically equivalent to rebuilt index'); + } + + return Object.freeze({ + valid: true, + eventCount: events.length, + entityCount: Object.values(snapshot.entities).reduce((sum, bucket) => sum + Object.keys(bucket).length, 0), + lastEventSequence: snapshot.lastEventSequence, + lastEventHash: snapshot.lastEventHash, + }); +} + +export function cleanupOrphanedStateTemps(rootDir = process.cwd()) { + const paths = statePaths(rootDir); + if (!fs.existsSync(paths.stateRoot)) return []; + const removed = []; + for (const name of fs.readdirSync(paths.stateRoot)) { + if (!name.includes('.tmp-')) continue; + const candidate = path.join(paths.stateRoot, name); + if (fs.statSync(candidate).isFile()) { + fs.unlinkSync(candidate); + removed.push(candidate); + } + } + return removed; +} + +export function getStateEnginePaths(rootDir = process.cwd()) { + return statePaths(rootDir); +} + +export function countStateEngineFiles(rootDir = process.cwd()) { + const paths = statePaths(rootDir); + if (!fs.existsSync(paths.stateRoot)) return 0; + return fs.readdirSync(paths.stateRoot, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name !== 'state.lock' && entry.name !== 'pending-commit.json' && !entry.name.includes('.tmp-')) + .length; +} + +export { stableJson as canonicalStateJson, sha256 as stateSha256 }; From 54de55f82c70d3b4c8aad6c4abd1d0d8d9eed462 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:28:22 +0200 Subject: [PATCH 003/177] feat(v0.13): port T04 candidate state-engine-migration.mjs --- .../orchestration/state-engine-migration.mjs | 940 ++++++++++++++++++ 1 file changed, 940 insertions(+) create mode 100644 runtime/orchestration/state-engine-migration.mjs diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs new file mode 100644 index 000000000..7ffcc4ada --- /dev/null +++ b/runtime/orchestration/state-engine-migration.mjs @@ -0,0 +1,940 @@ +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { validateWorkflowState } from '../autopilot/validators.mjs'; +import { validateOrchestrationRun } from './orchestration-run.mjs'; +import { + appendEntityState, + appendMetadataEvent, + canonicalStateJson, + ensureStateEngineLayout, + getStateEnginePaths, + loadCanonicalEvents, + loadEntityState, + loadStateSnapshot, + stateSha256, + verifyStateEngineIntegrity, +} from './state-engine-v2.mjs'; + +export const LEGACY_BACKUP_SCHEMA_VERSION = '1.0.0'; +export const LEGACY_MIGRATION_SCHEMA_VERSION = '1.0.0'; +export const LEGACY_BACKUP_FILE = 'legacy-backup.json'; + +export class StateMigrationError extends Error { + constructor(message, details = null) { + super(message); + this.name = 'StateMigrationError'; + this.details = details; + } +} + +function plainObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (!value || typeof value !== 'object') return value; + return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])); +} + +function stablePretty(value) { + return `${JSON.stringify(stable(value), null, 2)}\n`; +} + +function sha256(value) { + return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`; +} + +function readJson(filePath, label) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (error) { + throw new StateMigrationError(`${label} is invalid JSON: ${error.message}`); + } +} + +function projectRelative(rootDir, absolutePath) { + const root = path.resolve(rootDir); + const absolute = path.resolve(absolutePath); + const relative = path.relative(root, absolute).replaceAll('\\', '/'); + if (relative === '..' || relative.startsWith('../') || path.isAbsolute(relative)) { + throw new StateMigrationError('Legacy state path escapes project root'); + } + return relative; +} + +function resolveProjectPath(rootDir, relativePath) { + const root = path.resolve(rootDir); + const absolute = path.resolve(root, relativePath); + const relative = path.relative(root, absolute); + if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) { + throw new StateMigrationError(`Backup path escapes project root: ${relativePath}`); + } + return absolute; +} + +function atomicWrite(filePath, content) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + const temp = `${filePath}.tmp-${process.pid}-${crypto.randomUUID()}`; + const fd = fs.openSync(temp, 'wx'); + try { + fs.writeFileSync(fd, content, 'utf8'); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + fs.renameSync(temp, filePath); +} + +function listFilesRecursive(directory) { + if (!fs.existsSync(directory)) return []; + const files = []; + function walk(current) { + const entries = fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); + for (const entry of entries) { + const full = path.join(current, entry.name); + if (entry.isDirectory()) walk(full); + else if (entry.isFile()) files.push(full); + } + } + walk(directory); + return files; +} + +function collectLegacyBackupFiles(rootDir) { + const files = new Set(); + const autopilotState = path.join(rootDir, '.development-kit', 'autopilot', 'state'); + for (const file of listFilesRecursive(autopilotState)) { + const name = path.basename(file); + if (name === 'current.json' || /^revision-\d{6}\.json$/.test(name)) files.add(file); + } + + const runsRoot = path.join(rootDir, '.development-kit', 'runs'); + for (const file of listFilesRecursive(runsRoot)) { + const name = path.basename(file); + const parent = path.basename(path.dirname(file)); + if ( + name === 'manifest.json' + || name === 'current-state.json' + || name === 'final-state.json' + || (parent === 'state-revisions' && /^\d{8}\.json$/.test(name)) + ) files.add(file); + } + + return [...files].sort((a, b) => projectRelative(rootDir, a).localeCompare(projectRelative(rootDir, b))); +} + +function buildBackupBundle(rootDir, files) { + const entries = files.map((file) => { + const content = fs.readFileSync(file); + return { + path: projectRelative(rootDir, file), + fingerprint: stateSha256(content), + bytes: content.length, + contentBase64: content.toString('base64'), + }; + }); + const manifest = entries.map(({ path: entryPath, fingerprint, bytes }) => ({ path: entryPath, fingerprint, bytes })); + const sourceFingerprint = sha256(JSON.stringify(stable(manifest))); + return { + schemaVersion: LEGACY_BACKUP_SCHEMA_VERSION, + sourceFingerprint, + fileCount: entries.length, + files: entries, + }; +} + +function validateBackupBundle(bundle) { + if (!plainObject(bundle) || bundle.schemaVersion !== LEGACY_BACKUP_SCHEMA_VERSION) { + throw new StateMigrationError('Legacy backup bundle schema is invalid'); + } + if (typeof bundle.sourceFingerprint !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(bundle.sourceFingerprint)) { + throw new StateMigrationError('Legacy backup sourceFingerprint is invalid'); + } + if (!Array.isArray(bundle.files) || bundle.fileCount !== bundle.files.length) { + throw new StateMigrationError('Legacy backup file list is invalid'); + } + const manifest = []; + for (const entry of bundle.files) { + if (!plainObject(entry) || typeof entry.path !== 'string' || typeof entry.contentBase64 !== 'string') { + throw new StateMigrationError('Legacy backup entry is invalid'); + } + // Node substitutes U+FFFD for unpaired UTF-16 surrogates when mapping + // JavaScript paths to filesystem names. Distinct malicious strings can + // otherwise resolve to the same on-disk file and silently overwrite. + if (Buffer.from(entry.path, 'utf8').toString('utf8') !== entry.path) { + throw new StateMigrationError('Legacy backup path contains ill-formed Unicode'); + } + const pathParts = entry.path.split(/[/\\]+/).filter(Boolean); + if (pathParts.some((part) => part !== '.' && part !== '..' && (part.endsWith('.') || part.endsWith(' ')))) { + throw new StateMigrationError('Legacy backup path contains trailing dot or space: ' + entry.path); + } + const content = Buffer.from(entry.contentBase64, 'base64'); + if (content.length !== entry.bytes || stateSha256(content) !== entry.fingerprint) { + throw new StateMigrationError(`Legacy backup entry integrity failed: ${entry.path}`); + } + manifest.push({ path: entry.path, fingerprint: entry.fingerprint, bytes: entry.bytes }); + } + const actual = sha256(JSON.stringify(stable(manifest))); + if (actual !== bundle.sourceFingerprint) { + throw new StateMigrationError('Legacy backup sourceFingerprint does not match entries'); + } + return true; +} + +function persistLegacyBackup(rootDir, bundle) { + validateBackupBundle(bundle); + const paths = ensureStateEngineLayout(rootDir); + const backupPath = path.join(paths.stateRoot, LEGACY_BACKUP_FILE); + if (fs.existsSync(backupPath)) { + const existing = readJson(backupPath, 'Existing legacy backup'); + validateBackupBundle(existing); + if (existing.sourceFingerprint !== bundle.sourceFingerprint) { + throw new StateMigrationError('Refusing to overwrite legacy backup with different source state'); + } + return { backupPath, created: false }; + } + atomicWrite(backupPath, stablePretty(bundle)); + return { backupPath, created: true }; +} + +function autopilotHistory(rootDir) { + const stateDir = path.join(rootDir, '.development-kit', 'autopilot', 'state'); + if (!fs.existsSync(stateDir)) return null; + + const revisionFiles = fs.readdirSync(stateDir) + .filter((name) => /^revision-\d{6}\.json$/.test(name)) + .sort(); + + if (revisionFiles.length === 0) return null; + const currentPath = path.join(stateDir, 'current.json'); + if (!fs.existsSync(currentPath)) throw new StateMigrationError('Legacy Autopilot current.json is missing'); + + const pointer = readJson(currentPath, 'Legacy Autopilot current pointer'); + if (!Number.isInteger(pointer.currentRevision) || pointer.currentRevision < 1) { + throw new StateMigrationError('Legacy Autopilot currentRevision is invalid'); + } + const expectedCurrent = `revision-${String(pointer.currentRevision).padStart(6, '0')}.json`; + if (pointer.currentRevisionFile !== expectedCurrent) { + throw new StateMigrationError('Legacy Autopilot current pointer path is inconsistent'); + } + if (!revisionFiles.includes(expectedCurrent)) { + throw new StateMigrationError('Legacy Autopilot current revision file is missing'); + } + + for (let revision = 1; revision <= pointer.currentRevision; revision += 1) { + const expected = `revision-${String(revision).padStart(6, '0')}.json`; + if (!revisionFiles.includes(expected)) { + throw new StateMigrationError(`Legacy Autopilot revision chain is missing ${expected}`); + } + } + + const states = []; + for (const name of revisionFiles) { + const number = Number(name.match(/(\d{6})/)[1]); + if (number > pointer.currentRevision) { + throw new StateMigrationError('Legacy Autopilot contains revisions newer than current pointer'); + } + const state = readJson(path.join(stateDir, name), `Legacy Autopilot ${name}`); + try { validateWorkflowState(state); } catch (error) { + throw new StateMigrationError(`Legacy Autopilot state is invalid: ${name}: ${error.message}`); + } + if (state.stateRevision !== number) { + throw new StateMigrationError(`Legacy Autopilot revision filename/stateRevision mismatch: ${name}`); + } + if (state.workflowId !== pointer.workflowId) { + throw new StateMigrationError(`Legacy Autopilot workflow identity changes within revision chain: ${name}`); + } + states.push(state); + } + + const current = states.at(-1); + if (current.stateRevision !== pointer.currentRevision || current.workflowId !== pointer.workflowId) { + throw new StateMigrationError('Legacy Autopilot current state does not match pointer identity'); + } + + return { + entityType: 'autopilot-workflow', + entityId: current.workflowId, + refs: {}, + states, + current, + }; +} + +function orchestrationHistories(rootDir) { + const runsRoot = path.join(rootDir, '.development-kit', 'runs'); + if (!fs.existsSync(runsRoot)) return []; + const histories = []; + + const contractDirs = fs.readdirSync(runsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .sort((a, b) => a.name.localeCompare(b.name)); + + for (const contractEntry of contractDirs) { + const contractDir = path.join(runsRoot, contractEntry.name); + const runDirs = fs.readdirSync(contractDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .sort((a, b) => a.name.localeCompare(b.name)); + + for (const runEntry of runDirs) { + const runDir = path.join(contractDir, runEntry.name); + const manifestPath = path.join(runDir, 'manifest.json'); + if (!fs.existsSync(manifestPath)) continue; + const manifest = readJson(manifestPath, 'Legacy orchestration manifest'); + try { validateOrchestrationRun(manifest); } catch (error) { + throw new StateMigrationError(`Legacy orchestration manifest is invalid: ${error.message}`); + } + + const revisionDir = path.join(runDir, 'state-revisions'); + let states = []; + if (fs.existsSync(revisionDir)) { + const names = fs.readdirSync(revisionDir) + .filter((name) => /^\d{8}\.json$/.test(name)) + .sort(); + + if (names.length > 0) { + const pointerPath = path.join(runDir, 'current-state.json'); + if (!fs.existsSync(pointerPath)) throw new StateMigrationError('Legacy orchestration current-state.json is missing'); + const pointer = readJson(pointerPath, 'Legacy orchestration current pointer'); + if ( + pointer.contractId !== manifest.contractId + || pointer.runId !== manifest.runId + || !Number.isInteger(pointer.stateRevision) + || pointer.stateRevision < 1 + ) { + throw new StateMigrationError('Legacy orchestration current pointer identity is invalid'); + } + const expectedName = `${String(pointer.stateRevision).padStart(8, '0')}.json`; + if (pointer.revisionPath !== `state-revisions/${expectedName}` || !names.includes(expectedName)) { + throw new StateMigrationError('Legacy orchestration current pointer path is inconsistent'); + } + for (let revision = 1; revision <= pointer.stateRevision; revision += 1) { + const expected = `${String(revision).padStart(8, '0')}.json`; + if (!names.includes(expected)) { + throw new StateMigrationError(`Legacy orchestration revision chain is missing ${expected}`); + } + } + + for (const name of names) { + const number = Number(name.replace('.json', '')); + if (number > pointer.stateRevision) { + throw new StateMigrationError('Legacy orchestration contains revisions newer than current pointer'); + } + const state = readJson(path.join(revisionDir, name), `Legacy orchestration revision ${name}`); + try { validateOrchestrationRun(state); } catch (error) { + throw new StateMigrationError(`Legacy orchestration revision is invalid: ${name}: ${error.message}`); + } + if (state.stateRevision !== number) { + throw new StateMigrationError(`Legacy orchestration filename/stateRevision mismatch: ${name}`); + } + if (state.contractId !== manifest.contractId || state.runId !== manifest.runId) { + throw new StateMigrationError('Legacy orchestration revision identity does not match manifest'); + } + states.push(state); + } + if (canonicalStateJson(states[0]) !== canonicalStateJson(manifest)) { + throw new StateMigrationError('Legacy orchestration revision 1 differs from immutable manifest'); + } + } + } + + if (states.length === 0) states = [manifest]; + const current = states.at(-1); + + const finalPath = path.join(runDir, 'final-state.json'); + if (fs.existsSync(finalPath)) { + const finalState = readJson(finalPath, 'Legacy orchestration final-state'); + try { validateOrchestrationRun(finalState); } catch (error) { + throw new StateMigrationError(`Legacy orchestration final-state is invalid: ${error.message}`); + } + if (canonicalStateJson(finalState) !== canonicalStateJson(current)) { + throw new StateMigrationError('Legacy orchestration final-state differs from current state'); + } + } + + histories.push({ + entityType: 'orchestration-run', + entityId: `${manifest.contractId}/${manifest.runId}`, + refs: { + contractId: manifest.contractId, + taskId: manifest.taskId, + runId: manifest.runId, + }, + states, + current, + }); + } + } + + return histories; +} + +export function inspectLegacyState(rootDir = process.cwd()) { + const backupFiles = collectLegacyBackupFiles(rootDir); + const backup = buildBackupBundle(rootDir, backupFiles); + const histories = []; + const autopilot = autopilotHistory(rootDir); + if (autopilot) histories.push(autopilot); + histories.push(...orchestrationHistories(rootDir)); + + return Object.freeze({ + sourceFingerprint: backup.sourceFingerprint, + legacyFileCount: backup.fileCount, + backup, + histories, + entityCount: histories.length, + }); +} + +function migrationId(sourceFingerprint) { + return `mig-v2-${sourceFingerprint.slice('sha256:'.length, 'sha256:'.length + 24)}`; +} + +function completedMigration(sourceFingerprint, rootDir) { + const events = loadCanonicalEvents(rootDir); + return events.find((event) => ( + event.eventType === 'MIGRATION_COMPLETED' + && event.payload?.sourceFingerprint === sourceFingerprint + && event.payload?.semanticEquivalence === true + )) ?? null; +} + +function migrateHistory(history, rootDir) { + for (const legacyState of history.states) { + const current = loadEntityState(history.entityType, history.entityId, rootDir); + if (current) { + if (current.stateRevision > legacyState.stateRevision) continue; + if (current.stateRevision === legacyState.stateRevision) { + if (canonicalStateJson(current) !== canonicalStateJson(legacyState)) { + throw new StateMigrationError( + `Existing State Engine entity differs from legacy state at revision ${legacyState.stateRevision}: ${history.entityType}/${history.entityId}`, + ); + } + continue; + } + } + + appendEntityState({ + rootDir, + entityType: history.entityType, + entityId: history.entityId, + state: legacyState, + refs: history.refs, + actorClass: 'legacy-migration', + timestamp: legacyState.updatedAt ?? legacyState.createdAt ?? new Date().toISOString(), + }); + } +} + +function verifySemanticEquivalence(histories, rootDir) { + const mismatches = []; + for (const history of histories) { + const current = loadEntityState(history.entityType, history.entityId, rootDir); + if (canonicalStateJson(current) !== canonicalStateJson(history.current)) { + mismatches.push({ + entityType: history.entityType, + entityId: history.entityId, + expectedRevision: history.current.stateRevision, + actualRevision: current?.stateRevision ?? null, + }); + } + } + return { + equivalent: mismatches.length === 0, + mismatches, + }; +} + +export function migrateLegacyStateToV2({ + rootDir = process.cwd(), + completedAt = new Date().toISOString(), +} = {}) { + const inspection = inspectLegacyState(rootDir); + ensureStateEngineLayout(rootDir); + + const existingCompletion = completedMigration(inspection.sourceFingerprint, rootDir); + if (existingCompletion) { + verifyStateEngineIntegrity(rootDir); + const backupPath = path.join(getStateEnginePaths(rootDir).stateRoot, LEGACY_BACKUP_FILE); + if (!fs.existsSync(backupPath)) { + throw new StateMigrationError('Completed migration is missing its recoverable legacy backup'); + } + const backup = readJson(backupPath, 'Existing legacy backup'); + validateBackupBundle(backup); + if (backup.sourceFingerprint !== inspection.sourceFingerprint) { + throw new StateMigrationError('Completed migration backup no longer matches the legacy source fingerprint'); + } + + return Object.freeze({ + migrated: false, + idempotent: true, + migrationId: existingCompletion.payload.migrationId, + sourceFingerprint: inspection.sourceFingerprint, + legacyFileCount: inspection.legacyFileCount, + entityCount: inspection.entityCount, + semanticEquivalence: true, + backupPath, + }); + } + + const backupResult = persistLegacyBackup(rootDir, inspection.backup); + + for (const history of inspection.histories) migrateHistory(history, rootDir); + + const equivalence = verifySemanticEquivalence(inspection.histories, rootDir); + if (!equivalence.equivalent) { + throw new StateMigrationError('Legacy migration semantic-equivalence check failed', equivalence.mismatches); + } + + const id = migrationId(inspection.sourceFingerprint); + appendMetadataEvent({ + rootDir, + eventType: 'MIGRATION_COMPLETED', + actorClass: 'legacy-migration', + timestamp: completedAt, + payload: { + schemaVersion: LEGACY_MIGRATION_SCHEMA_VERSION, + migrationId: id, + sourceFingerprint: inspection.sourceFingerprint, + entityCount: inspection.entityCount, + legacyFileCount: inspection.legacyFileCount, + semanticEquivalence: true, + legacyRetained: true, + backupFile: LEGACY_BACKUP_FILE, + }, + }); + + verifyStateEngineIntegrity(rootDir); + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: false }); + const migration = snapshot.migrations.find((item) => item.migrationId === id); + if (!migration?.semanticEquivalence) { + throw new StateMigrationError('Migration completion is missing from reconstructed snapshot'); + } + + return Object.freeze({ + migrated: true, + idempotent: false, + migrationId: id, + sourceFingerprint: inspection.sourceFingerprint, + legacyFileCount: inspection.legacyFileCount, + entityCount: inspection.entityCount, + semanticEquivalence: true, + legacyRetained: true, + backupPath: backupResult.backupPath, + }); +} + +function resolveSafeRestoreDestination(targetRoot, relativePath, { createMissing = false } = {}) { + const root = path.resolve(targetRoot); + const destination = resolveProjectPath(root, relativePath); + if (createMissing) fs.mkdirSync(root, { recursive: true }); + if (!fs.existsSync(root)) return destination; + if (fs.lstatSync(root).isSymbolicLink()) { + throw new StateMigrationError('Legacy backup restore root must not be a symbolic link'); + } + const rootReal = fs.realpathSync(root); + const components = path.relative(root, destination).split(path.sep); + if (components.some((part) => !part || part === '.' || part === '..' || part.endsWith('.') || part.endsWith(' '))) { + throw new StateMigrationError('Invalid legacy backup restore path'); + } + let current = root; + for (const component of components.slice(0, -1)) { + current = path.join(current, component); + try { + const stat = fs.lstatSync(current); + if (stat.isSymbolicLink()) { + throw new StateMigrationError('Legacy backup restore path contains a symbolic link or junction: ' + relativePath); + } + if (!stat.isDirectory()) throw new StateMigrationError('Legacy backup restore parent is not a directory: ' + relativePath); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + if (createMissing) fs.mkdirSync(current); + else continue; + } + const real = fs.realpathSync(current); + const rel = path.relative(rootReal, real); + if (rel === '..' || rel.startsWith('..' + path.sep) || path.isAbsolute(rel)) { + throw new StateMigrationError('Legacy backup restore escapes its target root: ' + relativePath); + } + } + try { + const existing = fs.lstatSync(destination); + if (!existing.isFile() || existing.isSymbolicLink()) { + throw new StateMigrationError('Legacy backup restore destination must be a regular file: ' + relativePath); + } + if (existing.nlink > 1) { + throw new StateMigrationError('Legacy backup restore destination has multiple hard links: ' + relativePath); + } + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + return destination; +} + + +/** A deterministic sibling journal lets offline recovery find interrupted root promotion. */ +function restoreJournalPath(targetRoot) { + const root = path.resolve(targetRoot); + const digest = crypto.createHash('sha256').update(root).digest('hex').slice(0, 24); + return path.join(path.dirname(root), '.dk-restore-journal-' + digest + '.json'); +} + +/** + * Directory-entry ordering barrier for the restore transaction. + * + * POSIX: fsync(2) on the parent directory descriptor. + * Windows: libuv opens directories with FILE_FLAG_BACKUP_SEMANTICS, and + * fs.fsyncSync calls FlushFileBuffers. FlushFileBuffers needs a write-capable + * handle ('r+'); a read-only handle fails EPERM. Microsoft documents a normal + * (flags = 0) flush as writing cached file data *and metadata* and + * synchronizing the storage cache (NtFlushBuffersFileEx; only + * FLUSH_FLAGS_FILE_DATA_SYNC_ONLY is documented invalid for directory + * handles). See docs/04-architecture/dkf-t04-windows-restore-durability-protocol.md + * for the exact guarantee relied on and its limits. + * + * Any failure is fatal: callers never mutate or acknowledge past a barrier + * that did not complete, so the journal and witnesses are retained. + */ +function syncRestoreParent(parent) { + let fd; + try { + fd = fs.openSync(parent, process.platform === 'win32' ? 'r+' : 'r'); + fs.fsyncSync(fd); + } catch (error) { + throw new StateMigrationError('Restore directory flush barrier failed; recovery evidence retained', { + code: 'RESTORE_DIRECTORY_FLUSH_FAILED', directory: parent, cause: error.code || error.message, + }); + } finally { + if (fd !== undefined) fs.closeSync(fd); + } +} + +function durableRestoreRollbackRename(from, to) { + fs.renameSync(from, to); + syncRestoreParent(path.dirname(to)); +} + +/** + * Sync the staged file bytes and (on POSIX) every containing directory before + * the promotion journal becomes durable. A rename alone does not durably + * commit nested directory entries after sudden power loss. + */ +function syncRestoreTree(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const item = path.join(directory, entry.name); + if (entry.isSymbolicLink()) continue; // retain links as links, never follow + if (entry.isDirectory()) { + syncRestoreTree(item); + } else if (entry.isFile()) { + // FlushFileBuffers on Windows requires a write-capable handle; a + // read-only descriptor can fail EPERM even when the file is writable. + const fd = fs.openSync(item, process.platform === 'win32' ? 'r+' : 'r'); + try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); } + } else { + throw new StateMigrationError('Legacy restore stage contains an unsupported special file'); + } + } + syncRestoreParent(directory); +} + +function createRestoreJournal(file, data) { + const fd = fs.openSync(file, 'wx', 0o600); + try { + fs.writeFileSync(fd, stablePretty(data)); + fs.fsyncSync(fd); + } finally { fs.closeSync(fd); } + syncRestoreParent(path.dirname(file)); +} + +/** + * Recover only an interrupted OFFLINE restore for the exact target root. + * Reject unexpected names, inode witnesses, or concurrent target mutations. + */ +export function recoverLegacyRestore({ targetRoot, confirmOffline = false } = {}) { + if (typeof targetRoot !== 'string' || !targetRoot) { + throw new StateMigrationError('targetRoot is required for restore recovery'); + } + if (confirmOffline !== true) { + throw new StateMigrationError('Explicit offline confirmation is required for restore recovery'); + } + const root = path.resolve(targetRoot); + const parent = path.dirname(root); + const journalPath = restoreJournalPath(root); + if (!fs.existsSync(journalPath)) return Object.freeze({ recovered: false }); + const tx = readJson(journalPath, 'Legacy restore journal'); + const expectedStage = '.dk-legacy-restore-stage-' + tx.transactionId; + const stage = path.join(parent, expectedStage); + const displaced = stage + '-original'; + if (!/^[a-f0-9]{32}$/.test(tx.transactionId || '') || + tx.targetRoot !== root || tx.schemaVersion !== '1.0.0' || + tx.stageName !== expectedStage || tx.existed !== true && tx.existed !== false || + !/^\d+$/.test(tx.stageDev || '') || !/^\d+$/.test(tx.stageIno || '') || + (tx.existed && (!/^\d+$/.test(tx.originalDev || '') || !/^\d+$/.test(tx.originalIno || '')))) { + throw new StateMigrationError('Legacy restore journal contains an invalid transaction witness'); + } + // The recovery runs in an explicitly quiescent trusted workspace. All + // location/inode witnesses are rechecked before any cleanup. + const inspect = file => { + try { return fs.lstatSync(file, { bigint: true }); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } + }; + const atRoot = inspect(root); + const inStage = inspect(stage); + const inDisplaced = inspect(displaced); + const equal = (stat, dev, ino) => stat && stat.isDirectory() && !stat.isSymbolicLink() && + stat.dev.toString() === dev && stat.ino.toString() === ino; + if (inStage && !equal(inStage, tx.stageDev, tx.stageIno)) { + throw new StateMigrationError('Unrecognized staged restore directory; refusing recovery'); + } + if (inDisplaced && (!tx.existed || !equal(inDisplaced, tx.originalDev, tx.originalIno))) { + throw new StateMigrationError('Displaced restore directory differs from original witness'); + } + + let outcome; + // Pre-mutation capability probe (fails closed with every witness intact). + syncRestoreParent(parent); + if (inDisplaced && !atRoot) { + // The restored root directory entry must be durable before removing the + // only journal that permits redoing this rollback after another power loss. + durableRestoreRollbackRename(displaced, root); // crash after displacement + outcome = 'rolled-back-original'; + } else if (inDisplaced && equal(atRoot, tx.stageDev, tx.stageIno)) { + // Promotion completed; preserve original in displaced directory for + // explicit/manual audit rather than silently deleting recovery evidence. + outcome = 'promoted-original-retained'; + } else if (!inDisplaced && (!atRoot && !tx.existed || + (tx.existed && equal(atRoot, tx.originalDev, tx.originalIno)))) { + outcome = 'uncommitted-original-unchanged'; + } else if (!tx.existed && equal(atRoot, tx.stageDev, tx.stageIno)) { + outcome = 'promoted-new-root'; + } else { + throw new StateMigrationError('Ambiguous restore journal state; refusing automatic mutation'); + } + + if (inStage) { + fs.rmSync(stage, { recursive: true, force: true }); + syncRestoreParent(parent); + } + // Keep any promoted displaced original as a recovery asset: never auto-delete. + fs.unlinkSync(journalPath); + syncRestoreParent(parent); + return Object.freeze({ recovered: true, outcome, displacedOriginal: outcome === 'promoted-original-retained' ? displaced : null }); +} + +/** + * Snapshot descendant metadata before copying: cpSync recreates ownership + * under the maintenance identity and may update source directory atimes. + * Post-order records let directories receive their final times only after + * all nested file handles have been visited. + */ +function collectRestoreMetadata(source, relative = '') { + const stat = fs.lstatSync(source); + const children = stat.isDirectory() + ? fs.readdirSync(source).flatMap(name => collectRestoreMetadata( + path.join(source, name), relative ? path.join(relative, name) : name, + )) + : []; + return [...children, { + relative, mode: stat.mode & 0o7777, + uid: stat.uid, gid: stat.gid, + atime: stat.atime, mtime: stat.mtime, + kind: stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : stat.isSymbolicLink() ? 'symlink' : 'unsupported', + }]; +} + +function applyRestoreMetadata(stage, records) { + for (const entry of records) { + const destination = entry.relative ? path.join(stage, entry.relative) : stage; + const current = fs.lstatSync(destination); + const sameKind = entry.kind === 'directory' ? current.isDirectory() + : entry.kind === 'file' ? current.isFile() + : entry.kind === 'symlink' ? current.isSymbolicLink() : false; + if (!sameKind) throw new StateMigrationError('Staged restore object type differs from original metadata'); + if (process.platform !== 'win32' && + (current.uid !== entry.uid || current.gid !== entry.gid)) { + if (entry.kind === 'symlink') fs.lchownSync(destination, entry.uid, entry.gid); + else fs.chownSync(destination, entry.uid, entry.gid); + } + if (entry.kind === 'symlink') continue; // never follow + fs.chmodSync(destination, entry.mode); + fs.utimesSync(destination, entry.atime, entry.mtime); + // Persist restored metadata, and avoid readdir once parent atime is set. + if (entry.kind === 'directory') { + syncRestoreParent(destination); + } else if (process.platform !== 'win32') { + const fd = fs.openSync(destination, 'r'); + try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); } + } + } +} + +export function restoreLegacyBackup({ + backupPath, + targetRoot, + overwrite = false, + confirmOffline = false, +} = {}) { + if (typeof backupPath !== 'string' || !backupPath) throw new StateMigrationError('backupPath is required'); + if (typeof targetRoot !== 'string' || !targetRoot) throw new StateMigrationError('targetRoot is required'); + if (confirmOffline !== true) { + throw new StateMigrationError('Legacy backup restoration requires explicit offline confirmation and an exclusively controlled local workspace'); + } + if (fs.existsSync(restoreJournalPath(targetRoot))) { + throw new StateMigrationError('Incomplete legacy restore exists; run recoverLegacyRestore with offline confirmation first'); + } + const bundle = readJson(backupPath, 'Legacy backup'); + validateBackupBundle(bundle); + + // Complete a full destination security and overwrite preflight before writing + // even one byte; a later malicious backup path cannot leave a partial restore. + const destinations = bundle.files.map((entry) => ({ + entry, + destination: resolveSafeRestoreDestination(targetRoot, entry.path), + })); + const normalizedDestinations = new Set(); + for (const { entry, destination } of destinations) { + // Conservative case/Unicode folding and Win32 trailing-dot/space removal + // prevents duplicate-target aliases on APFS/HFS+, NTFS, and FAT. + // Refusing distinct-case or aliased names on a case-sensitive mount is intentional: + // restore cannot safely assume a particular target filesystem policy. + const key = destination + .normalize('NFC') + .toLowerCase() + .split(/[\\/]+/) + .map((part) => part.replace(/[. ]+$/, '')) + .join('/'); + if (normalizedDestinations.has(key)) { + throw new StateMigrationError('Duplicate normalized backup restore destination: ' + entry.path); + } + normalizedDestinations.add(key); + } + for (const { entry, destination } of destinations) { + if (fs.existsSync(destination) && !overwrite) { + throw new StateMigrationError('Refusing to overwrite restored legacy path: ' + entry.path); + } + } + + // Path-based preflight + O_NOFOLLOW cannot secure an overwrite against a + // concurrent parent-directory swap. Build and fsync a complete replacement + // tree in an owner-only sibling directory. Publish by renaming the root + // directory itself; no restored byte is ever opened through an attacker- + // mutable path *inside* targetRoot. + const resolvedRoot = path.resolve(targetRoot); + const parent = path.dirname(resolvedRoot); + const originalRootExists = fs.existsSync(resolvedRoot); + const rootStat = originalRootExists ? fs.lstatSync(resolvedRoot) : null; + const originalMetadata = originalRootExists ? collectRestoreMetadata(resolvedRoot) : []; + const rootIdentity = originalRootExists ? fs.lstatSync(resolvedRoot, { bigint: true }) : null; + if (rootStat && (!rootStat.isDirectory() || rootStat.isSymbolicLink())) { + throw new StateMigrationError('Legacy backup restore root must be an ordinary directory'); + } + const transactionId = crypto.randomBytes(16).toString('hex'); + // Pre-mutation capability probe: if this volume cannot complete the + // directory flush barrier, refuse before creating the stage or journal. + syncRestoreParent(parent); + const stage = path.join(parent, '.dk-legacy-restore-stage-' + transactionId); + fs.mkdirSync(stage, { mode: 0o700 }); + const displaced = stage + '-original'; + let promoted = false; + let displacedOriginal = false; + try { + fs.chmodSync(stage, 0o700); + if (originalRootExists) { + fs.cpSync(resolvedRoot, stage, { recursive: true, force: true, dereference: false, verbatimSymlinks: true }); + } + const restored = []; + for (const { entry } of destinations) { + const destination = resolveSafeRestoreDestination(stage, entry.path, { createMissing: true }); + const content = Buffer.from(entry.contentBase64, 'base64'); + const tempFile = destination + '.tmp-' + crypto.randomUUID(); + const fd = fs.openSync(tempFile, 'wx', 0o600); + try { + fs.writeFileSync(fd, content); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + // An atomic rename replaces the *directory entry* even if the prior + // staging entry is a link; it never truncates an external hardlink inode. + fs.renameSync(tempFile, destination); + if (stateSha256(fs.readFileSync(destination)) !== entry.fingerprint) { + throw new StateMigrationError('Staged legacy file failed fingerprint verification: ' + entry.path); + } + restored.push(entry.path); + } + + // Seal and flush staged file contents before metadata restoration. + // Metadata is replayed post-order so source file owners and descendant + // traversal modes do not drift and historical parent atimes are applied + // after every directory traversal has finished. + syncRestoreTree(stage); + if (rootStat) applyRestoreMetadata(stage, originalMetadata); + + // Durable journal precedes *both* root-directory renames. A crash after + // either rename is recoverable by an explicit offline recovery invocation. + const stageStat = fs.lstatSync(stage, { bigint: true }); + const journal = restoreJournalPath(resolvedRoot); + createRestoreJournal(journal, { + schemaVersion: '1.0.0', + transactionId, + stageName: path.basename(stage), + targetRoot: resolvedRoot, + existed: originalRootExists, + originalDev: rootIdentity?.dev.toString() ?? null, + originalIno: rootIdentity?.ino.toString() ?? null, + stageDev: stageStat.dev.toString(), + stageIno: stageStat.ino.toString(), + }); + + // Moving the existing root out of the way does not follow a symlink. + // Verify that the directory moved is still the original preflighted inode + // before publishing the staged replacement. + if (originalRootExists) { + fs.renameSync(resolvedRoot, displaced); + syncRestoreParent(parent); + displacedOriginal = true; + const moved = fs.lstatSync(displaced); + if (moved.dev !== rootStat.dev || moved.ino !== rootStat.ino) { + throw new StateMigrationError('Legacy backup restore root changed during staging'); + } + } + fs.renameSync(stage, resolvedRoot); + syncRestoreParent(parent); + promoted = true; + displacedOriginal = false; + // Preserve original root as a retained rollback asset. Only after the + // new root is durably visible should the journal be acknowledged. + fs.unlinkSync(journal); + syncRestoreParent(parent); + // Promotion is now acknowledged and durable. An old tree kept in the + // displaced slot is not needed for this completed transaction. + if (originalRootExists) { + try { fs.rmSync(displaced, { recursive: true, force: true }); } catch { + // Retain rather than damage the newly promoted root if cleanup fails. + } + } + return Object.freeze({ + restoredFiles: restored.sort(), + sourceFingerprint: bundle.sourceFingerprint, + }); + } finally { + if (displacedOriginal) { + try { + if (!fs.existsSync(resolvedRoot)) { + durableRestoreRollbackRename(displaced, resolvedRoot); + } + } catch {} + } + if (!promoted) { + try { fs.rmSync(stage, { recursive: true, force: true }); } catch {} + } + } +} + +export function getLegacyBackupPath(rootDir = process.cwd()) { + return path.join(getStateEnginePaths(rootDir).stateRoot, LEGACY_BACKUP_FILE); +} + +export { validateBackupBundle }; From a14d6a647078dcb31bb5a1396c59514dc10c91ef Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:28:25 +0200 Subject: [PATCH 004/177] feat(v0.13): port T04 candidate state-engine-event.schema.json --- schemas/state-engine-event.schema.json | 107 +++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 schemas/state-engine-event.schema.json diff --git a/schemas/state-engine-event.schema.json b/schemas/state-engine-event.schema.json new file mode 100644 index 000000000..97b77069b --- /dev/null +++ b/schemas/state-engine-event.schema.json @@ -0,0 +1,107 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://development-kit.dev/schemas/state-engine-event.schema.json", + "title": "Development Kit State Engine V2 Event", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "sequence", + "eventId", + "timestamp", + "actorClass", + "eventType", + "entityType", + "entityId", + "lifecycleId", + "taskId", + "contractId", + "runId", + "payload", + "previousEventHash", + "eventHash" + ], + "properties": { + "schemaVersion": { + "const": "1.0.0" + }, + "sequence": { + "type": "integer", + "minimum": 1 + }, + "eventId": { + "type": "string", + "pattern": "^evt-[0-9]{10}-[a-f0-9]{20}$" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "actorClass": { + "type": "string", + "minLength": 1 + }, + "eventType": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_]{1,95}$" + }, + "entityType": { + "type": [ + "string", + "null" + ] + }, + "entityId": { + "type": [ + "string", + "null" + ] + }, + "lifecycleId": { + "type": [ + "string", + "null" + ] + }, + "taskId": { + "type": [ + "string", + "null" + ] + }, + "contractId": { + "type": [ + "string", + "null" + ] + }, + "runId": { + "type": [ + "string", + "null" + ] + }, + "payload": { + "type": "object" + }, + "previousEventHash": { + "anyOf": [ + { + "$ref": "#/$defs/sha256" + }, + { + "type": "null" + } + ] + }, + "eventHash": { + "$ref": "#/$defs/sha256" + } + }, + "$defs": { + "sha256": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + } + } +} From f0ce90b1a91dd9978735846a9f3a07f692f6ea8a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:28:29 +0200 Subject: [PATCH 005/177] feat(v0.13): port T04 candidate state-engine-snapshot.schema.json --- schemas/state-engine-snapshot.schema.json | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 schemas/state-engine-snapshot.schema.json diff --git a/schemas/state-engine-snapshot.schema.json b/schemas/state-engine-snapshot.schema.json new file mode 100644 index 000000000..729cb03d2 --- /dev/null +++ b/schemas/state-engine-snapshot.schema.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://development-kit.dev/schemas/state-engine-snapshot.schema.json", + "title": "Development Kit State Engine V2 Snapshot", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "engineSchemaVersion", + "lastEventSequence", + "lastEventHash", + "entities", + "migrations" + ], + "properties": { + "schemaVersion": { + "const": "2.0.0" + }, + "engineSchemaVersion": { + "const": "2.0.0" + }, + "lastEventSequence": { + "type": "integer", + "minimum": 0 + }, + "lastEventHash": { + "anyOf": [ + { + "$ref": "#/$defs/sha256" + }, + { + "type": "null" + } + ] + }, + "entities": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "stateRevision", + "state", + "lastEventSequence", + "lastEventHash", + "refs" + ], + "properties": { + "stateRevision": { + "type": "integer", + "minimum": 1 + }, + "state": {}, + "lastEventSequence": { + "type": "integer", + "minimum": 1 + }, + "lastEventHash": { + "$ref": "#/$defs/sha256" + }, + "refs": { + "type": "object" + } + } + } + } + }, + "migrations": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "$defs": { + "sha256": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + } + } +} From 13ce619216897b9ac44c36697cc94ae7277c8587 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:28:33 +0200 Subject: [PATCH 006/177] feat(v0.13): port T04 candidate state-engine-index.schema.json --- schemas/state-engine-index.schema.json | 65 ++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 schemas/state-engine-index.schema.json diff --git a/schemas/state-engine-index.schema.json b/schemas/state-engine-index.schema.json new file mode 100644 index 000000000..d7ed58a2f --- /dev/null +++ b/schemas/state-engine-index.schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://development-kit.dev/schemas/state-engine-index.schema.json", + "title": "Development Kit State Engine V2 Disposable Index", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "sourceSequence", + "sourceHash", + "recordCount", + "records", + "byContract", + "byTask", + "byRun", + "byLifecycle" + ], + "properties": { + "schemaVersion": { + "const": "1.0.0" + }, + "sourceSequence": { + "type": "integer", + "minimum": 0 + }, + "sourceHash": { + "anyOf": [ + { + "$ref": "#/$defs/sha256" + }, + { + "type": "null" + } + ] + }, + "recordCount": { + "type": "integer", + "minimum": 0 + }, + "records": { + "type": "array", + "items": { + "type": "object" + } + }, + "byContract": { + "type": "object" + }, + "byTask": { + "type": "object" + }, + "byRun": { + "type": "object" + }, + "byLifecycle": { + "type": "object" + } + }, + "$defs": { + "sha256": { + "type": "string", + "pattern": "^sha256:[a-f0-9]{64}$" + } + } +} From 2107d94ce9201dc04f46db2839c917385885756e Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:28:38 +0200 Subject: [PATCH 007/177] feat(v0.13): port T04 candidate windows-durable-rename.ps1 --- scripts/windows-durable-rename.ps1 | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 scripts/windows-durable-rename.ps1 diff --git a/scripts/windows-durable-rename.ps1 b/scripts/windows-durable-rename.ps1 new file mode 100644 index 000000000..7fe8bc790 --- /dev/null +++ b/scripts/windows-durable-rename.ps1 @@ -0,0 +1,30 @@ +# Windows offline recovery helper. Uses the native directory-move API with +# MOVEFILE_WRITE_THROUGH; Node.js fs.renameSync exposes no equivalent flag. +# Accept only literal path arguments from the trusted DKF recovery runtime. +param( + [Parameter(Mandatory = $true)][string]$From, + [Parameter(Mandatory = $true)][string]$To +) + +$ErrorActionPreference = 'Stop' +if ([string]::IsNullOrWhiteSpace($From) -or [string]::IsNullOrWhiteSpace($To)) { + throw 'Both directory paths are required' +} +# The native API checks that the source exists and the destination does not. +# Avoid .NET file-path normalization of extended-length (\\?\) Win32 paths. + +$source = @' +using System; +using System.Runtime.InteropServices; +public static class DkfWindowsDurableRename { + [DllImport("kernel32.dll", EntryPoint = "MoveFileExW", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool MoveFileEx(string source, string destination, uint flags); +} +'@ +Add-Type -TypeDefinition $source -ErrorAction Stop +$MOVEFILE_WRITE_THROUGH = [uint32]8 +if (-not [DkfWindowsDurableRename]::MoveFileEx($From, $To, $MOVEFILE_WRITE_THROUGH)) { + $win32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw ([System.ComponentModel.Win32Exception]::new($win32Error)) +} From 80f1896876065073be665cc3df818074ead02185 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:28:54 +0200 Subject: [PATCH 008/177] test(v0.13): port T04 candidate state-engine-v2.test.mjs --- scripts/state-engine-v2.test.mjs | 372 +++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 scripts/state-engine-v2.test.mjs diff --git a/scripts/state-engine-v2.test.mjs b/scripts/state-engine-v2.test.mjs new file mode 100644 index 000000000..9bda942d0 --- /dev/null +++ b/scripts/state-engine-v2.test.mjs @@ -0,0 +1,372 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { + appendEntityState, + canonicalStateJson, + cleanupOrphanedStateTemps, + countStateEngineFiles, + getStateEnginePaths, + loadCanonicalEvents, + loadStateIndex, + loadStateSnapshot, + queryStateIndex, + rebuildSnapshotFromEvents, + rebuildStateIndex, + rebuildStateSnapshot, + verifyStateEngineIntegrity, +} from '../runtime/orchestration/state-engine-v2.mjs'; +import { + getCurrentState, + saveStateRevision, +} from '../runtime/autopilot/state-store.mjs'; +import { + loadCurrentRunState, + persistRunManifest, + persistRunStateRevision, +} from '../runtime/orchestration/orchestration-run.mjs'; + +function tempProject(t, prefix = 'dk-state-v2-') { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + t.after(() => fs.rmSync(rootDir, { recursive: true, force: true })); + return rootDir; +} + +function workflowState(revision = 1, overrides = {}) { + return { + schemaVersion: '1.0.0', + workflowId: 'wf-state-v2', + projectId: 'proj-state-v2', + workspaceId: 'ws-state-v2', + workflowMode: 'autopilot', + autonomyLevel: 'guided-autopilot', + workflowStatus: 'executing', + currentStage: revision > 1 ? 'DEFINE' : 'UNDERSTAND', + completedStages: revision > 1 ? ['UNDERSTAND'] : [], + skippedStages: [], + blockedStages: [], + activeAction: null, + pendingApproval: null, + pendingConfirmation: null, + activeDecisionMenu: null, + stateRevision: revision, + createdAt: '2026-10-07T18:00:00.000Z', + updatedAt: new Date(Date.parse('2026-10-07T18:00:00.000Z') + revision * 1000).toISOString(), + frameworkVersion: '0.11.2', + ...overrides, + }; +} + +function runState(revision = 1, overrides = {}) { + return { + schemaVersion: '1.0.0', + contractId: 'INC-STATE-V2', + taskId: 'TASK-STATE-V2', + runId: 'run-state-v2', + sourceFingerprint: 'sha256:' + 'a'.repeat(64), + createdAt: '2026-10-07T18:10:00.000Z', + updatedAt: new Date(Date.parse('2026-10-07T18:10:00.000Z') + revision * 1000).toISOString(), + stateRevision: revision, + state: revision === 1 ? 'READY' : 'IMPLEMENTING', + executionStrategy: 'sequential-fresh-context', + hostCapabilities: { + fileRead: true, + fileWrite: true, + shell: true, + git: true, + freshContext: true, + subagents: false, + parallelAgents: false, + browser: false, + visualInspection: false, + externalModelRouting: false, + }, + manualEvidenceRequired: false, + requiredGates: { + verification: ['specification', 'tests'], + reviewers: ['code-reviewer'], + controlDomains: [], + humanApprovals: [], + configurationReadiness: { required: false }, + }, + completedGates: [], + correctionAttempt: 0, + failureSignatures: [], + verificationVerdict: null, + acceptanceState: 'PENDING', + ...overrides, + }; +} + +test('AC-012 canonical event history is monotonic, hash-chained, and reconstructable', (t) => { + const rootDir = tempProject(t); + const first = workflowState(1); + const second = workflowState(2); + + appendEntityState({ + rootDir, + entityType: 'autopilot-workflow', + entityId: first.workflowId, + state: first, + actorClass: 'test', + timestamp: first.updatedAt, + }); + appendEntityState({ + rootDir, + entityType: 'autopilot-workflow', + entityId: second.workflowId, + state: second, + actorClass: 'test', + timestamp: second.updatedAt, + }); + + const events = loadCanonicalEvents(rootDir); + assert.equal(events.length, 2); + assert.equal(events[0].sequence, 1); + assert.equal(events[1].sequence, 2); + assert.equal(events[0].previousEventHash, null); + assert.equal(events[1].previousEventHash, events[0].eventHash); + + const rebuilt = rebuildSnapshotFromEvents(events); + assert.deepEqual( + rebuilt.entities['autopilot-workflow'][first.workflowId].state, + second, + ); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + +test('AC-012 canonical history detects mutation and reordering', (t) => { + const rootDir = tempProject(t); + appendEntityState({ + rootDir, + entityType: 'generic-state', + entityId: 'one', + state: { stateRevision: 1, value: 1 }, + actorClass: 'test', + timestamp: '2026-10-07T18:20:00.000Z', + }); + appendEntityState({ + rootDir, + entityType: 'generic-state', + entityId: 'one', + state: { stateRevision: 2, value: 2 }, + actorClass: 'test', + timestamp: '2026-10-07T18:20:01.000Z', + }); + + const paths = getStateEnginePaths(rootDir); + const lines = fs.readFileSync(paths.events, 'utf8').trim().split(/\r?\n/); + const mutated = JSON.parse(lines[0]); + mutated.payload.state.value = 99; + fs.writeFileSync(paths.events, JSON.stringify(mutated) + '\n' + lines[1] + '\n', 'utf8'); + assert.throws(() => loadCanonicalEvents(rootDir), /hash mismatch/i); + + fs.writeFileSync(paths.events, lines[1] + '\n' + lines[0] + '\n', 'utf8'); + assert.throws(() => loadCanonicalEvents(rootDir), /First state event sequence|sequence/i); +}); + +test('AC-012 canonical tail truncation fails closed when the materialized snapshot witnesses later history', (t) => { + const rootDir = tempProject(t); + appendEntityState({ + rootDir, + entityType: 'generic-state', + entityId: 'tail-witness', + state: { stateRevision: 1, value: 1 }, + actorClass: 'test', + timestamp: '2026-10-07T18:21:00.000Z', + }); + appendEntityState({ + rootDir, + entityType: 'generic-state', + entityId: 'tail-witness', + state: { stateRevision: 2, value: 2 }, + actorClass: 'test', + timestamp: '2026-10-07T18:21:01.000Z', + }); + + const paths = getStateEnginePaths(rootDir); + const lines = fs.readFileSync(paths.events, 'utf8').trim().split(/\r?\n/); + assert.equal(lines.length, 2); + + // Simulate loss/removal of the canonical tail while retaining the previously + // materialized snapshot as an integrity witness. + fs.writeFileSync(paths.events, lines[0] + '\n', 'utf8'); + + assert.throws( + () => loadStateSnapshot(rootDir, { rebuildIfNeeded: true }), + /history appears truncated/i, + ); +}); + +test('State Engine rejects a state-root symlink or junction that resolves outside the project', (t) => { + const rootDir = tempProject(t, 'dk-state-realpath-'); + const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'dk-state-outside-')); + t.after(() => fs.rmSync(outside, { recursive: true, force: true })); + + const dkDir = path.join(rootDir, '.development-kit'); + fs.mkdirSync(dkDir, { recursive: true }); + const statePath = path.join(dkDir, 'state'); + + try { + fs.symlinkSync(outside, statePath, process.platform === 'win32' ? 'junction' : 'dir'); + } catch (error) { + if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { + t.skip('Host does not permit directory symlink/junction creation'); + return; + } + throw error; + } + + assert.throws( + () => appendEntityState({ + rootDir, + entityType: 'generic-state', + entityId: 'escape-attempt', + state: { stateRevision: 1, value: true }, + actorClass: 'test', + timestamp: '2026-10-07T18:22:00.000Z', + }), + /resolves outside project root/, + ); + + assert.equal(fs.readdirSync(outside).length, 0); +}); + +test('tampered State Engine schema contract fails closed on read', (t) => { + const rootDir = tempProject(t); + const state = workflowState(1); + saveStateRevision(state, rootDir); + const paths = getStateEnginePaths(rootDir); + + const schema = JSON.parse(fs.readFileSync(paths.schema, 'utf8')); + schema.authority.canonicalHistory = 'something-else.jsonl'; + fs.writeFileSync(paths.schema, JSON.stringify(schema, null, 2) + '\n', 'utf8'); + + assert.throws( + () => getCurrentState(rootDir), + /schema\.json does not match the runtime schema contract/, + ); +}); + +test('AC-013 materialized snapshot deletion/corruption rebuilds from canonical history', (t) => { + const rootDir = tempProject(t); + const state = workflowState(1); + appendEntityState({ + rootDir, + entityType: 'autopilot-workflow', + entityId: state.workflowId, + state, + actorClass: 'test', + timestamp: state.updatedAt, + }); + + const paths = getStateEnginePaths(rootDir); + const expected = loadStateSnapshot(rootDir); + fs.rmSync(paths.snapshot); + const rebuiltAfterDelete = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); + assert.equal(canonicalStateJson(rebuiltAfterDelete), canonicalStateJson(expected)); + + fs.writeFileSync(paths.snapshot, '{"corrupt":true}\n', 'utf8'); + const rebuiltAfterCorruption = rebuildStateSnapshot(rootDir); + assert.equal(canonicalStateJson(rebuiltAfterCorruption), canonicalStateJson(expected)); +}); + +test('AC-014 disposable index deletion/rebuild preserves semantically equivalent query state', (t) => { + const rootDir = tempProject(t); + const run = runState(1); + appendEntityState({ + rootDir, + entityType: 'orchestration-run', + entityId: run.contractId + '/' + run.runId, + state: run, + refs: { contractId: run.contractId, taskId: run.taskId, runId: run.runId }, + actorClass: 'test', + timestamp: run.updatedAt, + }); + + const before = loadStateIndex(rootDir); + const queryBefore = queryStateIndex({ rootDir, contractId: run.contractId }); + const paths = getStateEnginePaths(rootDir); + fs.rmSync(paths.index); + const rebuilt = rebuildStateIndex(rootDir); + const queryAfter = queryStateIndex({ rootDir, contractId: run.contractId }); + + assert.equal(canonicalStateJson(rebuilt), canonicalStateJson(before)); + assert.deepEqual(queryAfter, queryBefore); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + +test('interrupted temporary state files cannot advance canonical history', (t) => { + const rootDir = tempProject(t); + const state = workflowState(1); + appendEntityState({ + rootDir, + entityType: 'autopilot-workflow', + entityId: state.workflowId, + state, + actorClass: 'test', + timestamp: state.updatedAt, + }); + const paths = getStateEnginePaths(rootDir); + const before = fs.readFileSync(paths.events, 'utf8'); + + fs.writeFileSync( + path.join(paths.stateRoot, 'events.jsonl.tmp-interrupted'), + before + '{"partial":', + 'utf8', + ); + + assert.equal(loadCanonicalEvents(rootDir).length, 1); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); + const removed = cleanupOrphanedStateTemps(rootDir); + assert.equal(removed.length, 1); + assert.equal(fs.readFileSync(paths.events, 'utf8'), before); +}); + +test('fresh Autopilot projects use State Engine V2 without legacy revision micro-files', (t) => { + const rootDir = tempProject(t); + const first = workflowState(1); + saveStateRevision(first, rootDir); + const second = workflowState(2); + saveStateRevision(second, rootDir); + + assert.deepEqual(getCurrentState(rootDir), second); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'autopilot', 'state')), false); + assert.equal(loadCanonicalEvents(rootDir).length, 2); + assert.equal(countStateEngineFiles(rootDir), 4); +}); + +test('fresh orchestration transition state uses V2 while manifest remains an immutable artifact', (t) => { + const rootDir = tempProject(t); + const run = runState(1); + persistRunManifest(run, rootDir); + const first = persistRunStateRevision(run, rootDir); + assert.equal(first.stateEngine, 'v2'); + + const second = runState(2); + const next = persistRunStateRevision(second, rootDir); + assert.equal(next.stateEngine, 'v2'); + assert.deepEqual(loadCurrentRunState(run.contractId, run.runId, rootDir), second); + + const runDir = path.join(rootDir, '.development-kit', 'runs', run.contractId, run.runId); + assert.equal(fs.existsSync(path.join(runDir, 'state-revisions')), false); + assert.equal(fs.existsSync(path.join(runDir, 'current-state.json')), false); + assert.equal(fs.existsSync(path.join(runDir, 'manifest.json')), true); +}); + +test('orchestration V2 refuses same-revision semantic overwrite', (t) => { + const rootDir = tempProject(t); + const run = runState(1); + persistRunManifest(run, rootDir); + persistRunStateRevision(run, rootDir); + + const forged = structuredClone(run); + forged.state = 'CORRECTING'; + assert.throws( + () => persistRunStateRevision(forged, rootDir), + /Refusing to overwrite orchestration run state revision/, + ); +}); From 20de1eb7766d97224adb840d76e6d4dcc7b8b7f8 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:28:58 +0200 Subject: [PATCH 009/177] test(v0.13): port T04 candidate state-engine-v2-migration.test.mjs --- scripts/state-engine-v2-migration.test.mjs | 1061 ++++++++++++++++++++ 1 file changed, 1061 insertions(+) create mode 100644 scripts/state-engine-v2-migration.test.mjs diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs new file mode 100644 index 000000000..fc624367c --- /dev/null +++ b/scripts/state-engine-v2-migration.test.mjs @@ -0,0 +1,1061 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync as requireSpawnSync } from 'node:child_process'; + +import { + appendEntityState, + canonicalStateJson, + countStateEngineFiles, + getStateEnginePaths, + loadCanonicalEvents, + loadEntityState, + verifyStateEngineIntegrity, +} from '../runtime/orchestration/state-engine-v2.mjs'; +import { + getLegacyBackupPath, + inspectLegacyState, + migrateLegacyStateToV2, + restoreLegacyBackup, + recoverLegacyRestore, +} from '../runtime/orchestration/state-engine-migration.mjs'; +import { + getCurrentState, + saveStateRevision, +} from '../runtime/autopilot/state-store.mjs'; +import { + loadCurrentRunState, +} from '../runtime/orchestration/orchestration-run.mjs'; + +function tempProject(t, prefix = 'dk-state-migration-') { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + t.after(() => fs.rmSync(rootDir, { recursive: true, force: true })); + return rootDir; +} + +function workflowState(revision, workflowId = 'wf-legacy-001') { + return { + schemaVersion: '1.0.0', + workflowId, + projectId: 'proj-legacy', + workspaceId: 'ws-legacy', + workflowMode: 'autopilot', + autonomyLevel: 'guided-autopilot', + workflowStatus: revision === 26 ? 'paused' : 'executing', + currentStage: revision >= 20 ? 'IMPLEMENT' : revision >= 10 ? 'DESIGN' : 'UNDERSTAND', + completedStages: revision >= 10 ? ['UNDERSTAND', 'DEFINE'] : [], + skippedStages: [], + blockedStages: [], + activeAction: null, + pendingApproval: null, + pendingConfirmation: null, + activeDecisionMenu: null, + stateRevision: revision, + createdAt: '2026-09-01T00:00:00.000Z', + updatedAt: new Date(Date.parse('2026-09-01T00:00:00.000Z') + revision * 1000).toISOString(), + frameworkVersion: '0.11.2', + }; +} + +function writeLegacyAutopilot(rootDir, count = 26) { + const stateDir = path.join(rootDir, '.development-kit', 'autopilot', 'state'); + fs.mkdirSync(stateDir, { recursive: true }); + const workflowId = 'wf-legacy-001'; + for (let revision = 1; revision <= count; revision += 1) { + const name = 'revision-' + String(revision).padStart(6, '0') + '.json'; + fs.writeFileSync( + path.join(stateDir, name), + JSON.stringify(workflowState(revision, workflowId), null, 2) + '\n', + 'utf8', + ); + } + fs.writeFileSync( + path.join(stateDir, 'current.json'), + JSON.stringify({ + currentRevision: count, + currentRevisionFile: 'revision-' + String(count).padStart(6, '0') + '.json', + workflowId, + updatedAt: '2026-09-01T01:00:00.000Z', + }, null, 2) + '\n', + 'utf8', + ); + return { stateDir, workflowId, current: workflowState(count, workflowId) }; +} + +function runState(revision, state = 'READY', acceptanceState = 'PENDING') { + return { + schemaVersion: '1.0.0', + contractId: 'INC-LEGACY-RUN', + taskId: 'TASK-LEGACY-RUN', + runId: 'run-legacy-001', + sourceFingerprint: 'sha256:' + 'b'.repeat(64), + createdAt: '2026-09-02T00:00:00.000Z', + updatedAt: new Date(Date.parse('2026-09-02T00:00:00.000Z') + revision * 1000).toISOString(), + stateRevision: revision, + state, + executionStrategy: 'sequential-fresh-context', + hostCapabilities: { + fileRead: true, + fileWrite: true, + shell: true, + git: true, + freshContext: true, + subagents: false, + parallelAgents: false, + browser: false, + visualInspection: false, + externalModelRouting: false, + }, + manualEvidenceRequired: false, + requiredGates: { + verification: ['specification', 'tests'], + reviewers: ['code-reviewer'], + controlDomains: [], + humanApprovals: [], + configurationReadiness: { required: false }, + }, + completedGates: state === 'ACCEPTED' ? ['specification', 'tests', 'code-reviewer'] : [], + correctionAttempt: 0, + failureSignatures: [], + verificationVerdict: state === 'ACCEPTED' ? 'PASS' : null, + acceptanceState, + }; +} + +function writeLegacyRun(rootDir) { + const run1 = runState(1, 'READY', 'PENDING'); + const run2 = runState(2, 'VERIFYING', 'PENDING'); + const run3 = runState(3, 'ACCEPTED', 'ACCEPTED'); + const runDir = path.join( + rootDir, + '.development-kit', + 'runs', + run1.contractId, + run1.runId, + ); + const revisions = path.join(runDir, 'state-revisions'); + fs.mkdirSync(revisions, { recursive: true }); + fs.writeFileSync(path.join(runDir, 'manifest.json'), JSON.stringify(run1, null, 2) + '\n', 'utf8'); + for (const run of [run1, run2, run3]) { + const name = String(run.stateRevision).padStart(8, '0') + '.json'; + fs.writeFileSync(path.join(revisions, name), JSON.stringify(run, null, 2) + '\n', 'utf8'); + } + fs.writeFileSync( + path.join(runDir, 'current-state.json'), + JSON.stringify({ + schemaVersion: '1.0.0', + contractId: run3.contractId, + runId: run3.runId, + stateRevision: run3.stateRevision, + revisionPath: 'state-revisions/00000003.json', + }, null, 2) + '\n', + 'utf8', + ); + fs.writeFileSync(path.join(runDir, 'final-state.json'), JSON.stringify(run3, null, 2) + '\n', 'utf8'); + return { runDir, current: run3 }; +} + +test('AC-015 unmigrated legacy Autopilot projects remain on legacy state until migration', (t) => { + const rootDir = tempProject(t); + const legacy = writeLegacyAutopilot(rootDir, 2); + assert.deepEqual(getCurrentState(rootDir), legacy.current); + + const third = workflowState(3, legacy.workflowId); + saveStateRevision(third, rootDir); + assert.equal( + fs.existsSync(path.join(legacy.stateDir, 'revision-000003.json')), + true, + ); + assert.deepEqual(getCurrentState(rootDir), third); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false); +}); + +test('AC-015 partial V2 import cannot cut over an existing legacy project before MIGRATION_COMPLETED', (t) => { + const rootDir = tempProject(t); + const legacy = writeLegacyAutopilot(rootDir, 2); + + const partial = workflowState(2, legacy.workflowId); + partial.currentStage = 'REVIEW'; + appendEntityState({ + rootDir, + entityType: 'autopilot-workflow', + entityId: legacy.workflowId, + state: partial, + actorClass: 'legacy-migration', + timestamp: partial.updatedAt, + }); + + // A partially imported V2 entity exists, but there is no verified + // MIGRATION_COMPLETED event. The legacy store must therefore remain canonical. + const beforeCompletion = getCurrentState(rootDir); + assert.equal(beforeCompletion.currentStage, legacy.current.currentStage); + assert.notEqual(beforeCompletion.currentStage, partial.currentStage); + + const nextLegacy = workflowState(3, legacy.workflowId); + saveStateRevision(nextLegacy, rootDir); + assert.equal( + fs.existsSync(path.join(legacy.stateDir, 'revision-000003.json')), + true, + ); + assert.equal(getCurrentState(rootDir).stateRevision, 3); +}); + +test('AC-015 legacy migration is idempotent, semantically equivalent, and retains recoverable legacy state', (t) => { + const rootDir = tempProject(t); + const legacy = writeLegacyAutopilot(rootDir, 26); + const inspection = inspectLegacyState(rootDir); + assert.equal(inspection.legacyFileCount, 27); + assert.equal(inspection.entityCount, 1); + + const first = migrateLegacyStateToV2({ + rootDir, + completedAt: '2026-10-07T18:30:00.000Z', + }); + assert.equal(first.migrated, true); + assert.equal(first.idempotent, false); + assert.equal(first.semanticEquivalence, true); + assert.equal(first.legacyRetained, true); + assert.equal(canonicalStateJson(getCurrentState(rootDir)), canonicalStateJson(legacy.current)); + assert.equal(canonicalStateJson( + loadEntityState('autopilot-workflow', legacy.workflowId, rootDir), + ), canonicalStateJson(legacy.current)); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); + + for (let revision = 1; revision <= 26; revision += 1) { + assert.equal( + fs.existsSync(path.join( + legacy.stateDir, + 'revision-' + String(revision).padStart(6, '0') + '.json', + )), + true, + ); + } + assert.equal(fs.existsSync(path.join(legacy.stateDir, 'current.json')), true); + assert.equal(fs.existsSync(getLegacyBackupPath(rootDir)), true); + + const eventsBefore = loadCanonicalEvents(rootDir).length; + const second = migrateLegacyStateToV2({ + rootDir, + completedAt: '2026-10-07T18:31:00.000Z', + }); + assert.equal(second.migrated, false); + assert.equal(second.idempotent, true); + assert.equal(loadCanonicalEvents(rootDir).length, eventsBefore); +}); + +test('AC-015 completed migration remains idempotent after legitimate V2 progress', (t) => { + const rootDir = tempProject(t); + const legacy = writeLegacyAutopilot(rootDir, 3); + const first = migrateLegacyStateToV2({ + rootDir, + completedAt: '2026-10-07T18:31:30.000Z', + }); + assert.equal(first.migrated, true); + + const progressed = workflowState(4, legacy.workflowId); + progressed.currentStage = 'VERIFY'; + appendEntityState({ + rootDir, + entityType: 'autopilot-workflow', + entityId: legacy.workflowId, + state: progressed, + actorClass: 'autopilot', + timestamp: progressed.updatedAt, + }); + + const eventCountBefore = loadCanonicalEvents(rootDir).length; + const rerun = migrateLegacyStateToV2({ + rootDir, + completedAt: '2026-10-07T18:31:40.000Z', + }); + + assert.equal(rerun.migrated, false); + assert.equal(rerun.idempotent, true); + assert.equal(loadCanonicalEvents(rootDir).length, eventCountBefore); + assert.equal(getCurrentState(rootDir).stateRevision, 4); + assert.equal(getCurrentState(rootDir).currentStage, 'VERIFY'); +}); + +test('AC-015 bundled legacy backup can restore the complete legacy state chain', (t) => { + const rootDir = tempProject(t); + writeLegacyAutopilot(rootDir, 5); + migrateLegacyStateToV2({ + rootDir, + completedAt: '2026-10-07T18:32:00.000Z', + }); + + const restoreRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'dk-state-restore-')); + t.after(() => fs.rmSync(restoreRoot, { recursive: true, force: true })); + const restored = restoreLegacyBackup({ confirmOffline: true, + backupPath: getLegacyBackupPath(rootDir), + targetRoot: restoreRoot, + }); + assert.equal(restored.restoredFiles.length, 6); + + for (let revision = 1; revision <= 5; revision += 1) { + assert.equal(fs.existsSync(path.join( + restoreRoot, + '.development-kit', + 'autopilot', + 'state', + 'revision-' + String(revision).padStart(6, '0') + '.json', + )), true); + } + assert.equal(fs.existsSync(path.join( + restoreRoot, + '.development-kit', + 'autopilot', + 'state', + 'current.json', + )), true); +}); + +test('AC-015 missing Autopilot revision fails closed before migration cutover', (t) => { + const rootDir = tempProject(t); + const legacy = writeLegacyAutopilot(rootDir, 4); + fs.rmSync(path.join(legacy.stateDir, 'revision-000002.json')); + + assert.throws( + () => migrateLegacyStateToV2({ rootDir }), + /revision chain is missing revision-000002\.json/, + ); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false); +}); + +test('AC-015 missing orchestration revision and manifest mismatch fail closed', (t) => { + const rootDir = tempProject(t); + const legacy = writeLegacyRun(rootDir); + fs.rmSync(path.join(legacy.runDir, 'state-revisions', '00000002.json')); + + assert.throws( + () => migrateLegacyStateToV2({ rootDir }), + /revision chain is missing 00000002\.json/, + ); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false); + + // Restore revision 2 and then corrupt revision 1 relative to manifest. + const run2 = runState(2, 'VERIFYING', 'PENDING'); + fs.writeFileSync( + path.join(legacy.runDir, 'state-revisions', '00000002.json'), + JSON.stringify(run2, null, 2) + '\n', + 'utf8', + ); + const run1 = runState(1, 'IMPLEMENTING', 'PENDING'); + fs.writeFileSync( + path.join(legacy.runDir, 'state-revisions', '00000001.json'), + JSON.stringify(run1, null, 2) + '\n', + 'utf8', + ); + + assert.throws( + () => migrateLegacyStateToV2({ rootDir }), + /revision 1 differs from immutable manifest/, + ); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false); +}); + +test('AC-015 corrupted legacy input fails closed before State Engine cutover', (t) => { + const rootDir = tempProject(t); + const legacy = writeLegacyAutopilot(rootDir, 3); + fs.writeFileSync(path.join(legacy.stateDir, 'revision-000002.json'), '{"broken":', 'utf8'); + + assert.throws( + () => migrateLegacyStateToV2({ rootDir }), + /invalid JSON/, + ); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false); + assert.equal(fs.existsSync(path.join(legacy.stateDir, 'current.json')), true); +}); + +test('AC-015 partial orchestration import cannot override legacy current state before verified migration completion', (t) => { + const rootDir = tempProject(t); + const legacy = writeLegacyRun(rootDir); + + const partial = structuredClone(legacy.current); + partial.state = 'BLOCKED'; + partial.acceptanceState = 'BLOCKED'; + appendEntityState({ + rootDir, + entityType: 'orchestration-run', + entityId: partial.contractId + '/' + partial.runId, + state: partial, + refs: { + contractId: partial.contractId, + taskId: partial.taskId, + runId: partial.runId, + }, + actorClass: 'legacy-migration', + timestamp: partial.updatedAt, + }); + + const current = loadCurrentRunState( + legacy.current.contractId, + legacy.current.runId, + rootDir, + ); + assert.equal(current.state, 'ACCEPTED'); + assert.equal(current.acceptanceState, 'ACCEPTED'); +}); + +test('AC-015 orchestration migration preserves ACCEPTED meaning and switches current-state reads to V2', (t) => { + const rootDir = tempProject(t); + const legacy = writeLegacyRun(rootDir); + const before = loadCurrentRunState( + legacy.current.contractId, + legacy.current.runId, + rootDir, + ); + assert.equal(before.state, 'ACCEPTED'); + assert.equal(before.acceptanceState, 'ACCEPTED'); + + const migrated = migrateLegacyStateToV2({ + rootDir, + completedAt: '2026-10-07T18:33:00.000Z', + }); + assert.equal(migrated.semanticEquivalence, true); + + const v2 = loadEntityState( + 'orchestration-run', + legacy.current.contractId + '/' + legacy.current.runId, + rootDir, + ); + assert.equal(canonicalStateJson(v2), canonicalStateJson(legacy.current)); + + const after = loadCurrentRunState( + legacy.current.contractId, + legacy.current.runId, + rootDir, + ); + assert.equal(after.state, 'ACCEPTED'); + assert.equal(after.acceptanceState, 'ACCEPTED'); + assert.equal(after.verificationVerdict, 'PASS'); + + assert.equal(fs.existsSync(path.join(legacy.runDir, 'state-revisions', '00000003.json')), true); + assert.equal(fs.existsSync(path.join(legacy.runDir, 'current-state.json')), true); + assert.equal(fs.existsSync(path.join(legacy.runDir, 'final-state.json')), true); +}); + +test('AC-034 representative 26-revision migration exceeds the >=80% active canonical micro-file reduction objective', (t) => { + const rootDir = tempProject(t); + writeLegacyAutopilot(rootDir, 26); + const legacyCount = inspectLegacyState(rootDir).legacyFileCount; + assert.equal(legacyCount, 27); + + migrateLegacyStateToV2({ + rootDir, + completedAt: '2026-10-07T18:34:00.000Z', + }); + + const canonicalFiles = countStateEngineFiles(rootDir); + const reductionPercent = Number((((legacyCount - canonicalFiles) / legacyCount) * 100).toFixed(2)); + process.stdout.write('T04_STATE_FILE_EVIDENCE ' + JSON.stringify({ + legacyTransitionStateFiles: legacyCount, + stateEngineCanonicalFilesIncludingRecoveryBundle: canonicalFiles, + reductionPercent, + legacyFilesRetainedForRecovery: true, + }) + '\n'); + + assert.equal(canonicalFiles, 5); + assert.equal(reductionPercent, 81.48); + assert.ok(reductionPercent >= 80); + + const paths = getStateEnginePaths(rootDir); + for (const required of ['events', 'snapshot', 'index', 'schema']) { + assert.equal(fs.existsSync(paths[required]), true); + } + assert.equal(fs.existsSync(getLegacyBackupPath(rootDir)), true); +}); + + +test('T04-H05 restore refuses a preexisting symlink/junction parent and leaves outside directory untouched', (t) => { + const rootDir = tempProject(t, 'dk-restore-source-'); + writeLegacyAutopilot(rootDir, 2); + const migrated = migrateLegacyStateToV2({ rootDir }); + const restoreRoot = tempProject(t, 'dk-restore-target-'); + const outside = tempProject(t, 'dk-restore-outside-'); + const dkRoot = path.join(restoreRoot, '.development-kit'); + fs.mkdirSync(dkRoot, { recursive: true }); + try { + fs.symlinkSync(outside, path.join(dkRoot, 'autopilot'), process.platform === 'win32' ? 'junction' : 'dir'); + } catch (error) { + if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { + t.skip('Host does not permit directory symlink/junction creation'); + return; + } + throw error; + } + assert.throws( + () => restoreLegacyBackup({ confirmOffline: true, backupPath: migrated.backupPath, targetRoot: restoreRoot }), + /symbolic link|junction|escapes/i, + ); + assert.deepEqual(fs.readdirSync(outside), []); +}); + +test('T04-H06 restore refuses a final-file symlink even when overwrite is explicitly enabled', (t) => { + const rootDir = tempProject(t, 'dk-restore-source-'); + writeLegacyAutopilot(rootDir, 2); + const migrated = migrateLegacyStateToV2({ rootDir }); + const restoreRoot = tempProject(t, 'dk-restore-target-'); + const outside = tempProject(t, 'dk-restore-outside-'); + const filename = path.join(outside, 'should-not-change.json'); + fs.writeFileSync(filename, 'SAFE', 'utf8'); + const parent = path.join(restoreRoot, '.development-kit', 'autopilot', 'state'); + fs.mkdirSync(parent, { recursive: true }); + try { + fs.symlinkSync(filename, path.join(parent, 'revision-000001.json'), 'file'); + } catch (error) { + if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { + t.skip('Host does not permit file symlink creation'); + return; + } + throw error; + } + assert.throws( + () => restoreLegacyBackup({ confirmOffline: true, backupPath: migrated.backupPath, targetRoot: restoreRoot, overwrite: true }), + /regular file|symbolic link|junction/i, + ); + assert.equal(fs.readFileSync(filename, 'utf8'), 'SAFE'); +}); + + +test('T04-H13 backup restore rejects a hard-linked overwrite destination', (t) => { + const rootDir = tempProject(t, 'dk-hardlink-source-'); + writeLegacyAutopilot(rootDir, 2); + const migrated = migrateLegacyStateToV2({ rootDir }); + const restoreRoot = tempProject(t, 'dk-hardlink-target-'); + const outside = tempProject(t, 'dk-hardlink-outside-'); + const external = path.join(outside, 'protected.json'); + fs.writeFileSync(external, 'PROTECTED', 'utf8'); + const destinationDir = path.join(restoreRoot, '.development-kit', 'autopilot', 'state'); + fs.mkdirSync(destinationDir, { recursive: true }); + const destination = path.join(destinationDir, 'revision-000001.json'); + try { + fs.linkSync(external, destination); + } catch (error) { + if (['EPERM', 'EACCES', 'ENOTSUP', 'EXDEV'].includes(error.code)) { + t.skip('Filesystem disallows hard-link fixture'); + return; + } + throw error; + } + assert.throws( + () => restoreLegacyBackup({ confirmOffline: true, backupPath: migrated.backupPath, targetRoot: restoreRoot, overwrite: true }), + /hard.?link|multiple links/i, + ); + assert.equal(fs.readFileSync(external, 'utf8'), 'PROTECTED'); +}); + + +test('T04-H19 restoring a backup cannot write through a destination hardlink swapped at open time', (t) => { + const source = tempProject(t, 'dk-restore-race-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const targetRoot = tempProject(t, 'dk-restore-race-target-'); + const protectedRoot = tempProject(t, 'dk-restore-race-outside-'); + const victim = path.join(protectedRoot, 'external.json'); + fs.writeFileSync(victim, 'EXTERNAL-CONTENT', 'utf8'); + const parent = path.join(targetRoot, '.development-kit', 'autopilot', 'state'); + fs.mkdirSync(parent, {recursive:true}); + const destination = path.join(parent, 'revision-000001.json'); + fs.writeFileSync(destination, 'stale value', 'utf8'); + const originalOpen = fs.openSync; + let swapped = false; + fs.openSync = function swappedAtDestinationOpen(file, flags, ...rest) { + if (!swapped && file === destination) { + swapped = true; + fs.rmSync(destination); + fs.linkSync(victim, destination); + } + return originalOpen.call(this, file, flags, ...rest); + }; + try { + const restored = restoreLegacyBackup({ confirmOffline: true, backupPath:migrated.backupPath, targetRoot, overwrite:true }); + assert.ok(restored.restoredFiles.includes('.development-kit/autopilot/state/revision-000001.json')); + } finally { + fs.openSync = originalOpen; + } + assert.equal(fs.readFileSync(victim, 'utf8'), 'EXTERNAL-CONTENT'); + assert.equal(JSON.parse(fs.readFileSync(destination, 'utf8')).stateRevision, 1); +}); + + +test('T04-H20 restore never creates target-tree directories before atomic staged promotion', (t) => { + const source = tempProject(t, 'dk-restore-stage-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const targetRoot = tempProject(t, 'dk-restore-stage-target-'); + const originalMkdir = fs.mkdirSync; + let attemptedUnsafeWrite = false; + fs.mkdirSync = function blockPrepromotionTargetWrites(directory, ...args) { + const relative = path.relative(targetRoot, path.resolve(directory)); + if (relative && relative !== '..' && !relative.startsWith('..' + path.sep) + && !path.isAbsolute(relative)) { + attemptedUnsafeWrite = true; + throw new Error('restore preflight attempted a direct target-directory mutation'); + } + return originalMkdir.call(this, directory, ...args); + }; + try { + const restored = restoreLegacyBackup({ confirmOffline: true, backupPath: migrated.backupPath, targetRoot }); + assert.equal(restored.restoredFiles.length, 3); + } finally { + fs.mkdirSync = originalMkdir; + } + assert.equal(attemptedUnsafeWrite, false); + const pathToRestored = path.join(targetRoot, '.development-kit', 'autopilot', 'state', 'revision-000001.json'); + assert.equal(JSON.parse(fs.readFileSync(pathToRestored, 'utf8')).stateRevision, 1); +}); + + +test('T04-H21 stage-promoted restore supports an absent root within an existing parent directory', (t) => { + const source = tempProject(t, 'dk-restore-newroot-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const parent = tempProject(t, 'dk-restore-newroot-parent-'); + const root = path.join(parent, 'new-restore-root'); + assert.equal(fs.existsSync(root), false); + const restored = restoreLegacyBackup({ confirmOffline: true, backupPath: migrated.backupPath, targetRoot: root }); + assert.equal(restored.restoredFiles.length, 3); + assert.equal( + JSON.parse(fs.readFileSync(path.join(root, '.development-kit','autopilot','state','revision-000001.json'), 'utf8')).stateRevision, + 1, + ); +}); + + +test('T04-H23 staged restore preserves the existing destination root mode and ownership', (t) => { + if (process.platform === 'win32') { + t.skip('POSIX mode/uid semantics are not portable to Windows'); return; + } + const source = tempProject(t, 'dk-mode-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const targetRoot = tempProject(t, 'dk-mode-target-'); + fs.chmodSync(targetRoot, 0o755); + const before = fs.statSync(targetRoot); + restoreLegacyBackup({ confirmOffline: true, backupPath: migrated.backupPath, targetRoot }); + const after = fs.statSync(targetRoot); + assert.equal(after.mode & 0o7777, before.mode & 0o7777); + assert.equal(after.uid, before.uid); + assert.equal(after.gid, before.gid); + assert.equal( + JSON.parse(fs.readFileSync(path.join(targetRoot,'.development-kit','autopilot','state','revision-000001.json'),'utf8')).stateRevision, + 1, + ); +}); + + +test('T04-H24 legacy restore requires explicit offline capability', (t) => { + const source = tempProject(t, 'dk-offline-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const root = tempProject(t, 'dk-offline-target-'); + assert.throws(() => restoreLegacyBackup({ backupPath: migrated.backupPath, targetRoot: root }), + /offline confirmation/i); + assert.deepEqual(fs.readdirSync(root), []); +}); + +test('T04-H25 interrupted two-step root promotion can restore original using journal', (t) => { + const source = tempProject(t, 'dk-crash-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const root = tempProject(t, 'dk-crash-target-'); + const sentinel = path.join(root, 'persist-original.txt'); + fs.writeFileSync(sentinel, 'ORIGINAL'); + const originalRename = fs.renameSync; + let movedRoot = false; + let failureInjected = false; + fs.renameSync = function injectPowerFailure(oldPath, nextPath, ...args) { + if (movedRoot && !failureInjected && typeof oldPath === 'string' && + oldPath.includes('.dk-legacy-restore-stage-') && nextPath === root) { + failureInjected = true; + throw new Error('simulated crash between displacement and promotion'); + } + if (oldPath === root && String(nextPath).endsWith('-original')) movedRoot = true; + return originalRename.call(this, oldPath, nextPath, ...args); + }; + try { + assert.throws(() => restoreLegacyBackup({ + confirmOffline: true, backupPath: migrated.backupPath, targetRoot: root, + }), /simulated crash/); + } finally { fs.renameSync = originalRename; } + // If the in-process finally managed the rollback, the leftover journal is + // still authoritative. Recovery must be idempotent and must never erase it. + assert.equal(fs.readFileSync(sentinel, 'utf8'), 'ORIGINAL'); + assert.throws(() => restoreLegacyBackup({ + confirmOffline:true, backupPath:migrated.backupPath, targetRoot:root, + }), /Incomplete legacy restore/i); + assert.throws(() => recoverLegacyRestore({ targetRoot: root }), /offline confirmation/i); + const recovered = recoverLegacyRestore({ targetRoot: root, confirmOffline: true }); + assert.equal(recovered.recovered, true); + assert.equal(fs.readFileSync(sentinel,'utf8'), 'ORIGINAL'); + assert.equal(recoverLegacyRestore({ targetRoot: root, confirmOffline: true }).recovered, false); + const restored = restoreLegacyBackup({ + confirmOffline:true, backupPath:migrated.backupPath, targetRoot:root, + }); + assert.equal(restored.restoredFiles.length, 3); + assert.equal(fs.readFileSync(path.join(root,'persist-original.txt'),'utf8'),'ORIGINAL'); +}); + + +test('T04-H28 restore journals preserve original after a simulated process death between root renames', (t) => { + const source = tempProject(t, 'dk-crash-worker-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const root = tempProject(t, 'dk-crash-worker-target-'); + const sentinel = path.join(root, 'original.txt'); + fs.writeFileSync(sentinel, 'KEEP-ME', 'utf8'); + + const script = [ + 'import fs from "node:fs";', + 'import {restoreLegacyBackup} from '+JSON.stringify(new URL('../runtime/orchestration/state-engine-migration.mjs', import.meta.url).href)+';', + 'const backup=process.argv[1],root=process.argv[2],rename=fs.renameSync;', + 'fs.renameSync=function(a,b,...rest){', + ' if(a===root && String(b).endsWith("-original")) {const r=rename.call(this,a,b,...rest);process.exit(91);}', + ' return rename.call(this,a,b,...rest);', + '};', + 'restoreLegacyBackup({backupPath:backup,targetRoot:root,confirmOffline:true});', + ].join('\n'); + // spawnSync is intentionally used for a true process exit instead of a + // caught synchronous exception that runs finally/rollback. + const result = requireSpawnSync(process.execPath, ['--input-type=module','-e',script,migrated.backupPath,root]); + assert.equal(result.status, 91, result.stderr); + assert.equal(fs.existsSync(root), false); + assert.throws(() => restoreLegacyBackup({ + backupPath:migrated.backupPath,targetRoot:root,confirmOffline:true, + }), /Incomplete legacy restore/i); + const recovered = recoverLegacyRestore({ targetRoot: root, confirmOffline: true }); + assert.equal(recovered.outcome, 'rolled-back-original'); + assert.equal(fs.readFileSync(sentinel,'utf8'),'KEEP-ME'); + assert.equal(fs.existsSync(root),true); +}); + + +test('T04-H47 recovery fsyncs the restored parent entry before deleting its rollback journal', (t) => { + // Runs on Windows too: the barrier is FlushFileBuffers on a directory handle. + const source = tempProject(t, 'dk-rollback-fsync-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const root = tempProject(t, 'dk-rollback-fsync-target-'); + const sentinel = path.join(root, 'original.txt'); + fs.writeFileSync(sentinel, 'ROLLBACK-ORIGINAL', 'utf8'); + + // A real process exit avoids running the normal in-process finally rollback. + const script = [ + 'import fs from "node:fs";', + 'import {restoreLegacyBackup} from ' + + JSON.stringify(new URL('../runtime/orchestration/state-engine-migration.mjs', import.meta.url).href) + ';', + 'const backup=process.argv[1],root=process.argv[2],rename=fs.renameSync;', + 'fs.renameSync=function(a,b,...rest){', + ' if(a===root && String(b).endsWith("-original")) {rename.call(this,a,b,...rest);process.exit(91);}', + ' return rename.call(this,a,b,...rest);', + '};', + 'restoreLegacyBackup({backupPath:backup,targetRoot:root,confirmOffline:true});', + ].join('\n'); + const result = requireSpawnSync(process.execPath, + ['--input-type=module', '-e', script, migrated.backupPath, root]); + assert.equal(result.status, 91, result.stderr); + assert.equal(fs.existsSync(root), false); + + const parent = path.dirname(root); + const parentStat = fs.statSync(parent); + const originalRename = fs.renameSync; + const originalFsync = fs.fsyncSync; + const originalUnlink = fs.unlinkSync; + let rollbackRenamed = false; + let parentSyncedAfterRollback = false; + let journalRemoved = false; + fs.renameSync = function watchRollback(from, to, ...args) { + const returned = originalRename.call(this, from, to, ...args); + if (typeof from === 'string' && from.endsWith('-original') && to === root) { + rollbackRenamed = true; + } + return returned; + }; + fs.fsyncSync = function watchParentDurability(fd) { + const stat = fs.fstatSync(fd); + if (rollbackRenamed && stat.isDirectory() && + stat.dev === parentStat.dev && stat.ino === parentStat.ino) { + parentSyncedAfterRollback = true; + } + return originalFsync.call(this, fd); + }; + fs.unlinkSync = function guardJournalRemoval(target, ...args) { + if (typeof target === 'string' && + path.basename(target).startsWith('.dk-restore-journal-')) { + journalRemoved = true; + assert.equal(rollbackRenamed, true, 'Recovery must restore the displaced root'); + assert.equal(parentSyncedAfterRollback, true, + 'Rollback directory rename must be fsynced before deleting the journal'); + } + return originalUnlink.call(this, target, ...args); + }; + try { + const recovery = recoverLegacyRestore({ targetRoot: root, confirmOffline: true }); + assert.equal(recovery.outcome, 'rolled-back-original'); + assert.ok(journalRemoved); + assert.equal(fs.readFileSync(sentinel, 'utf8'), 'ROLLBACK-ORIGINAL'); + } finally { + fs.renameSync = originalRename; + fs.fsyncSync = originalFsync; + fs.unlinkSync = originalUnlink; + } +}); + +test('T04-H48 Windows crash rollback and recovery verify parent flush barrier before journal unlink', (t) => { + if (process.platform !== 'win32') { + t.skip('Windows parent flush barrier verification'); + return; + } + + const source = tempProject(t, 'dk-win-rollback-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const root = tempProject(t, 'dk-win-rollback-target-'); + const sentinel = path.join(root, 'original.txt'); + fs.writeFileSync(sentinel, 'WINDOWS-ORIGINAL', 'utf8'); + + const script = [ + 'import fs from "node:fs";', + 'import {restoreLegacyBackup} from ' + + JSON.stringify(new URL('../runtime/orchestration/state-engine-migration.mjs', import.meta.url).href) + ';', + 'const backup=process.argv[1],root=process.argv[2],rename=fs.renameSync;', + 'fs.renameSync=function(a,b,...rest){', + ' if(a===root && String(b).endsWith("-original")) {rename.call(this,a,b,...rest);process.exit(91);}', + ' return rename.call(this,a,b,...rest);', + '};', + 'restoreLegacyBackup({backupPath:backup,targetRoot:root,confirmOffline:true});', + ].join('\n'); + const result = requireSpawnSync(process.execPath, + ['--input-type=module', '-e', script, migrated.backupPath, root]); + assert.equal(result.status, 91, result.stderr); + assert.equal(fs.existsSync(root), false); + + const parent = path.dirname(root); + const parentIdentity = fs.statSync(parent); + const originalRename = fs.renameSync; + const originalFsync = fs.fsyncSync; + const originalUnlink = fs.unlinkSync; + let rollbackRenamed = false; + let parentFlushedAfterRollback = false; + let journalRemoved = false; + + fs.renameSync = function watchRollback(from, to, ...args) { + const returned = originalRename.call(this, from, to, ...args); + if (typeof from === 'string' && from.endsWith('-original') && to === root) { + rollbackRenamed = true; + } + return returned; + }; + fs.fsyncSync = function watchParentFlush(fd) { + const stat = fs.fstatSync(fd); + const returned = originalFsync.call(this, fd); + if (rollbackRenamed && stat.isDirectory() && + stat.dev === parentIdentity.dev && stat.ino === parentIdentity.ino) { + parentFlushedAfterRollback = true; + } + return returned; + }; + fs.unlinkSync = function guardJournalRemoval(target, ...args) { + if (typeof target === 'string' && + path.basename(target).startsWith('.dk-restore-journal-')) { + journalRemoved = true; + assert.equal(rollbackRenamed, true, 'Recovery must restore the displaced root'); + assert.equal(parentFlushedAfterRollback, true, + 'Windows rollback directory rename must be flushed to disk before deleting the journal'); + } + return originalUnlink.call(this, target, ...args); + }; + try { + const recovered = recoverLegacyRestore({ targetRoot: root, confirmOffline: true }); + assert.equal(recovered.outcome, 'rolled-back-original'); + assert.ok(journalRemoved); + assert.equal(fs.readFileSync(sentinel, 'utf8'), 'WINDOWS-ORIGINAL'); + } finally { + fs.renameSync = originalRename; + fs.fsyncSync = originalFsync; + fs.unlinkSync = originalUnlink; + } +}); + +test('T04-H30 offline staged restore preserves descendant file identity and POSIX permission metadata', (t) => { + if (process.platform === 'win32') { + t.skip('POSIX ownership and mode semantics require a POSIX host'); return; + } + const source = tempProject(t, 'dk-descendant-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const root = tempProject(t, 'dk-descendant-target-'); + const nested = path.join(root, 'unrelated', 'private.txt'); + fs.mkdirSync(path.dirname(nested), { recursive: true }); + fs.writeFileSync(nested, 'PROTECTED-CONTENT'); + fs.chmodSync(nested, 0o600); + if (process.getuid?.() === 0) { + fs.chownSync(nested, 65534, 65534); + } + const before = fs.statSync(nested); + restoreLegacyBackup({ backupPath:migrated.backupPath, targetRoot:root, confirmOffline:true }); + const after = fs.statSync(nested); + assert.equal(after.uid, before.uid); + assert.equal(after.gid, before.gid); + assert.equal(after.mode & 0o7777, before.mode & 0o7777); + assert.equal(fs.readFileSync(nested, 'utf8'), 'PROTECTED-CONTENT'); +}); + +test('T04-H31 staged restore does not advance historical root access and modification timestamps', (t) => { + if (process.platform === 'win32') { + t.skip('POSIX access-time semantics require a POSIX host'); return; + } + const source = tempProject(t, 'dk-atime-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const root = tempProject(t, 'dk-atime-target-'); + const old = new Date('2001-01-01T00:00:00.000Z'); + fs.utimesSync(root, old, old); + const before = fs.statSync(root); + restoreLegacyBackup({ backupPath:migrated.backupPath, targetRoot:root, confirmOffline:true }); + const after = fs.statSync(root); + assert.ok(Math.abs(after.atimeMs - before.atimeMs) < 1000, 'root access time must be preserved'); + assert.ok(Math.abs(after.mtimeMs - before.mtimeMs) < 1000, 'root modification time must be preserved'); +}); + + +test('T04-H32 offline staged restore preserves the text of unrelated relative symlinks', (t) => { + const source = tempProject(t, 'dk-link-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const targetRoot = tempProject(t, 'dk-link-target-'); + fs.writeFileSync(path.join(targetRoot, 'target.txt'), 'ORIGINAL', 'utf8'); + const link = path.join(targetRoot, 'relative-link.txt'); + try { + fs.symlinkSync('target.txt', link, 'file'); + } catch (error) { + if (['EPERM','EACCES','ENOTSUP'].includes(error.code)) { t.skip('Relative symlink unavailable'); return; } + throw error; + } + const priorLink = fs.readlinkSync(link); + restoreLegacyBackup({ backupPath: migrated.backupPath, targetRoot, confirmOffline: true }); + assert.equal(fs.readlinkSync(link), priorLink); + assert.equal(fs.readFileSync(link, 'utf8'), 'ORIGINAL'); +}); + +test('T04-H33 backup with duplicate normalized destinations is refused before staging', async (t) => { + const source = tempProject(t, 'dk-dup-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const targetRoot = tempProject(t, 'dk-dup-target-'); + const pathA = path.join(targetRoot, '.development-kit', 'autopilot', 'state', 'revision-000001.json'); + const backup = JSON.parse(fs.readFileSync(migrated.backupPath, 'utf8')); + const first = backup.files[0]; + const duplicate = { ...first, path: './' + first.path }; + backup.files.push(duplicate); + backup.fileCount = backup.files.length; + const manifest = backup.files.map(e => ({ path: e.path, fingerprint: e.fingerprint, bytes: e.bytes })); + const crypto = await import('node:crypto'); + function canonical(value) { + if (Array.isArray(value)) return value.map(canonical); + if (value && typeof value === 'object') return Object.fromEntries( + Object.entries(value).sort(([a],[b]) => a.localeCompare(b)).map(([key,val]) => [key, canonical(val)]) + ); + return value; + } + backup.sourceFingerprint = 'sha256:' + crypto.createHash('sha256').update(JSON.stringify(canonical(manifest))).digest('hex'); + const hostileBackup = path.join(source, 'alias-backup.json'); + fs.writeFileSync(hostileBackup, JSON.stringify(backup)); + assert.throws( + () => restoreLegacyBackup({ backupPath: hostileBackup, targetRoot, confirmOffline: true }), + /duplicate normalized/i, + ); + assert.equal(fs.existsSync(pathA), false); +}); + + +test('T04-H34 backup case aliases fail closed even on a case-sensitive test filesystem', async (t) => { + const source = tempProject(t, 'dk-restore-case-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const targetRoot = tempProject(t, 'dk-restore-case-target-'); + const backup = JSON.parse(fs.readFileSync(migrated.backupPath, 'utf8')); + const first = backup.files[0]; + const alias = { ...first, path: first.path.toUpperCase() }; + assert.notEqual(alias.path, first.path); + backup.files.push(alias); + backup.fileCount = backup.files.length; + const manifest = backup.files.map(e => ({ path: e.path, fingerprint: e.fingerprint, bytes: e.bytes })); + const { createHash } = await import('node:crypto'); + function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value && typeof value === 'object') return Object.fromEntries( + Object.keys(value).sort().map(key => [key, stable(value[key])])); + return value; + } + backup.sourceFingerprint = 'sha256:' + createHash('sha256').update(JSON.stringify(stable(manifest))).digest('hex'); + const aliasBackup = path.join(source, 'case-alias-backup.json'); + fs.writeFileSync(aliasBackup, JSON.stringify(backup)); + assert.throws( + () => restoreLegacyBackup({backupPath:aliasBackup,targetRoot,confirmOffline:true}), + /duplicate normalized/i, + ); + assert.deepEqual(fs.readdirSync(targetRoot), []); +}); + + +test('T04-H35 backup with distinct unpaired surrogates cannot overwrite one filesystem name', async (t) => { + const source = tempProject(t, 'dk-unicode-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const targetRoot = tempProject(t, 'dk-unicode-target-'); + const backup = JSON.parse(fs.readFileSync(migrated.backupPath, 'utf8')); + const sourceEntry = backup.files[0]; + backup.files.push({ ...sourceEntry, path: 'suspicious-\uD800.json' }); + backup.files.push({ ...sourceEntry, path: 'suspicious-\uD801.json' }); + backup.fileCount = backup.files.length; + const entries = backup.files.map(e => ({path:e.path, fingerprint:e.fingerprint, bytes:e.bytes})); + const { createHash } = await import('node:crypto'); + function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map(k => [k, stable(value[k])])); + } + return value; + } + backup.sourceFingerprint = 'sha256:' + createHash('sha256').update(JSON.stringify(stable(entries))).digest('hex'); + const hostileBackup = path.join(source, 'ill-formed-unicode.json'); + fs.writeFileSync(hostileBackup, JSON.stringify(backup)); + assert.throws( + () => restoreLegacyBackup({ backupPath: hostileBackup, targetRoot, confirmOffline: true }), + /ill-formed Unicode/i, + ); + assert.deepEqual(fs.readdirSync(targetRoot), []); +}); + +test('T04-H39 backup with trailing dot or space path alias is refused before restore', async (t) => { + const source = tempProject(t, 'dk-trailing-dot-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const targetRoot = tempProject(t, 'dk-trailing-dot-target-'); + const backup = JSON.parse(fs.readFileSync(migrated.backupPath, 'utf8')); + const sourceEntry = backup.files[0]; + // Attempt to add a trailing-dot alias path that would map to the same file on Windows + backup.files.push({ ...sourceEntry, path: sourceEntry.path + '.' }); + backup.fileCount = backup.files.length; + const entries = backup.files.map(e => ({ path: e.path, fingerprint: e.fingerprint, bytes: e.bytes })); + const { createHash } = await import('node:crypto'); + function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map(k => [k, stable(value[k])])); + } + return value; + } + backup.sourceFingerprint = 'sha256:' + createHash('sha256').update(JSON.stringify(stable(entries))).digest('hex'); + const hostileBackup = path.join(source, 'trailing-dot-backup.json'); + fs.writeFileSync(hostileBackup, JSON.stringify(backup)); + assert.throws( + () => restoreLegacyBackup({ backupPath: hostileBackup, targetRoot, confirmOffline: true }), + /trailing dot|duplicate normalized/i, + ); + assert.deepEqual(fs.readdirSync(targetRoot), []); +}); From 5f80674e5d7e4f05ce00afeaf42dcc0307f65d89 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:29:01 +0200 Subject: [PATCH 010/177] test(v0.13): port T04 candidate state-engine-v2-hardening.test.mjs --- scripts/state-engine-v2-hardening.test.mjs | 1032 ++++++++++++++++++++ 1 file changed, 1032 insertions(+) create mode 100644 scripts/state-engine-v2-hardening.test.mjs diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs new file mode 100644 index 000000000..251cf55e1 --- /dev/null +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -0,0 +1,1032 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { createHash } from 'node:crypto'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; + +import { + appendEntityState, + appendMetadataEvent, + getStateEnginePaths, + loadCanonicalEvents, + loadStateSnapshot, + verifyStateEngineIntegrity, + recoverAbandonedStateLock, + ensureStateEngineLayout, +} from '../runtime/orchestration/state-engine-v2.mjs'; + +function tempProject(t) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dk-t04-hardening-')); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + return dir; +} + +test('T04-H01 physical event ledger retains its open file identity across commits', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const ledger = getStateEnginePaths(rootDir).events; + const descriptor = fs.openSync(ledger, 'r'); + try { + const before = fs.fstatSync(descriptor).size; + appendMetadataEvent({ rootDir, eventType: 'FOLLOWUP_EVENT', payload: { value: 2 } }); + assert.ok(fs.fstatSync(descriptor).size > before, 'append must grow the same canonical file, not rewrite its inode'); + assert.equal(loadCanonicalEvents(rootDir).length, 2); + } finally { + fs.closeSync(descriptor); + } +}); + +test('T04-H02 a live owner cannot lose an aged lock', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const lockFile = getStateEnginePaths(rootDir).lock; + fs.writeFileSync(lockFile, JSON.stringify({ + owner: 'live-long-running-worker', + pid: process.pid, + hostname: os.hostname(), + acquiredAt: '2026-10-08T00:00:00.000Z', + }), 'utf8'); + const old = new Date(Date.now() - 30_000); + fs.utimesSync(lockFile, old, old); + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'MUST_NOT_COMMIT', payload: { value: 2 } }), + /lock acquisition timed out/i, + ); + assert.equal(loadCanonicalEvents(rootDir).length, 1); + assert.equal(fs.existsSync(lockFile), true); +}); + +function runWorker(rootDir, workerId) { + return new Promise((resolve, reject) => { + const moduleUrl = new URL('../runtime/orchestration/state-engine-v2.mjs', import.meta.url).href; + const worker = [ + 'import { appendEntityState } from ' + JSON.stringify(moduleUrl) + ';', + 'const dir = process.argv[1], id = process.argv[2];', + 'for (let n = 1; n <= 12; n++) appendEntityState({ rootDir: dir, entityType: "worker-state", entityId: id, state: { stateRevision: n, value: n }, actorClass: "concurrency-test" });', + ].join('\n'); + const child = spawn(process.execPath, ['--input-type=module', '-e', worker, rootDir, workerId], { stdio: ['ignore', 'pipe', 'pipe'] }); + let stderr = ''; + child.stderr.on('data', data => { stderr += data; }); + child.on('error', reject); + child.on('close', code => code === 0 ? resolve() : reject(new Error('Worker ' + workerId + ' exited ' + code + ': ' + stderr.slice(-1600)))); + }); +} + +test('T04-H03 four processes append without losing events or corrupting sequence', async (t) => { + const rootDir = tempProject(t); + await Promise.all(Array.from({ length: 4 }, (_, i) => runWorker(rootDir, 'worker-' + i))); + const events = loadCanonicalEvents(rootDir); + assert.equal(events.length, 48); + assert.equal(events[0].sequence, 1); + assert.equal(events.at(-1).sequence, 48); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + + + +test('T04-H04 torn append with a valid pending journal deterministically repairs without losing committed history', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const before = fs.readFileSync(paths.events, 'utf8'); + appendMetadataEvent({ rootDir, eventType: 'FOLLOWUP_EVENT', payload: { value: 2 } }); + const expected = fs.readFileSync(paths.events, 'utf8'); + const suffix = expected.slice(before.length); + fs.writeFileSync(paths.events, before + suffix.slice(0, 23), 'utf8'); + fs.writeFileSync(paths.pending, JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(before), + previousHash: 'sha256:' + createHash('sha256').update(before).digest('hex'), + suffix, + }), 'utf8'); + + const recovered = loadCanonicalEvents(rootDir); + assert.equal(recovered.length, 2); + assert.equal(fs.readFileSync(paths.events, 'utf8'), expected); + assert.equal(fs.existsSync(paths.pending), false); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + +test('T04-H07 conflicting pending suffix must fail closed, not truncate canonical evidence', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const before = fs.readFileSync(paths.events, 'utf8'); + fs.writeFileSync(paths.events, before + 'tampered', 'utf8'); + fs.writeFileSync(paths.pending, JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(before), + previousHash: 'sha256:' + createHash('sha256').update(before).digest('hex'), + suffix: '{"valid":true}\n', + }), 'utf8'); + + assert.throws(() => loadCanonicalEvents(rootDir), /suffix conflicts/i); + assert.ok(fs.readFileSync(paths.events, 'utf8').endsWith('tampered')); + assert.equal(fs.existsSync(paths.pending), true); +}); + +test('T04-H08 stale lock from a provably dead local process can be recovered', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + fs.writeFileSync(paths.lock, JSON.stringify({ + owner: 'dead-worker', + pid: 2147483647, + hostname: os.hostname(), + acquiredAt: '2026-10-08T00:00:00.000Z', + }), 'utf8'); + const old = new Date(Date.now() - 30_000); + fs.utimesSync(paths.lock, old, old); + appendMetadataEvent({ rootDir, eventType: 'RECOVERED_LOCK', payload: { value: 2 } }); + assert.equal(loadCanonicalEvents(rootDir).length, 2); +}); + + +test('T04-H09 internal canonical events file may not be redirected through a symlink', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const outsideRoot = tempProject(t); + const outsideFile = path.join(outsideRoot, 'outside-ledger.jsonl'); + const originalContent = fs.readFileSync(paths.events, 'utf8'); + fs.writeFileSync(outsideFile, originalContent, 'utf8'); + fs.rmSync(paths.events); + try { + fs.symlinkSync(outsideFile, paths.events, 'file'); + } catch (error) { + if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { + t.skip('Host does not permit file symlink creation'); + return; + } + throw error; + } + assert.throws(() => loadCanonicalEvents(rootDir), /not a regular file/i); + assert.throws(() => appendMetadataEvent({ rootDir, eventType: 'DANGER', payload: {} }), /not a regular file/i); + assert.equal(fs.readFileSync(outsideFile, 'utf8'), originalContent); +}); + + +test('T04-H10 appending to a valid ledger without a terminal newline preserves record separation', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const original = fs.readFileSync(paths.events, 'utf8'); + assert.ok(original.endsWith('\n')); + fs.writeFileSync(paths.events, original.slice(0, -1), 'utf8'); + appendMetadataEvent({ rootDir, eventType: 'FOLLOWUP_EVENT', payload: { value: 2 } }); + const history = loadCanonicalEvents(rootDir); + assert.equal(history.length, 2); + assert.equal(history[1].previousEventHash, history[0].eventHash); + assert.ok(fs.readFileSync(paths.events, 'utf8').endsWith('\n')); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + +test('T04-H11 missing aged lock during stale-owner recheck retries acquisition safely', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + fs.writeFileSync(paths.lock, JSON.stringify({ + owner: 'provably-dead-owner', + pid: 2147483647, + hostname: os.hostname(), + acquiredAt: '2026-10-08T00:00:00.000Z', + })); + const aged = new Date(Date.now() - 30_000); + fs.utimesSync(paths.lock, aged, aged); + const originalStat = fs.statSync; + let lockStatCalls = 0; + fs.statSync = function injectConcurrentLockRemoval(target, ...args) { + if (target === paths.lock && ++lockStatCalls === 2) { + const err = new Error('simulated competing stale-lock reclamation'); + err.code = 'ENOENT'; + throw err; + } + return originalStat.call(this, target, ...args); + }; + try { + appendMetadataEvent({ rootDir, eventType: 'AFTER_STALE_LOCK_RACE', payload: { ok: true } }); + } finally { + fs.statSync = originalStat; + } + assert.ok(lockStatCalls >= 3); + assert.equal(loadCanonicalEvents(rootDir).length, 2); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + + +test('T04-H12 snapshot reader tolerates a legitimate writer committing between ledger and snapshot reads', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const originalRead = fs.readFileSync; + let insertedWriter = false; + fs.readFileSync = function simulateReadWriteInterleave(file, ...args) { + if (!insertedWriter && file === paths.snapshot) { + insertedWriter = true; + appendMetadataEvent({ rootDir, eventType: 'SIMULTANEOUS_EVENT', payload: { value: 2 } }); + } + return originalRead.call(this, file, ...args); + }; + let snapshot; + try { + snapshot = loadStateSnapshot(rootDir); + } finally { + fs.readFileSync = originalRead; + } + assert.equal(insertedWriter, true); + assert.equal(snapshot.lastEventSequence, 2); + assert.equal(loadCanonicalEvents(rootDir).length, 2); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + + +test('T04-H14 transient Windows EPERM on lock creation retries without bypassing ownership', (t) => { + if (process.platform !== 'win32') { + t.skip('Windows-specific filesystem contention semantics'); + return; + } + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const originalOpen = fs.openSync; + let attempts = 0; + fs.openSync = function injectTemporaryWindowsLockContention(file, flags, ...args) { + if (file === paths.lock && flags === 'wx') { + attempts += 1; + if (attempts === 1) { + const err = new Error('simulated Windows transient lock-file sharing violation'); + err.code = 'EPERM'; + throw err; + } + } + return originalOpen.call(this, file, flags, ...args); + }; + try { + appendMetadataEvent({ rootDir, eventType: 'LOCK_RETRY_SUCCEEDED', payload: { value: 2 } }); + } finally { + fs.openSync = originalOpen; + } + assert.ok(attempts >= 2); + assert.equal(loadCanonicalEvents(rootDir).length, 2); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + + +test('T04-H15 simultaneous stale-lock reclaimers cannot delete a new owner', async (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + fs.writeFileSync(paths.lock, JSON.stringify({ + owner: 'dead-before-contention', pid: 2147483647, hostname: os.hostname(), + acquiredAt: '2026-10-08T00:00:00.000Z', + })); + const aged = new Date(Date.now() - 30_000); + fs.utimesSync(paths.lock, aged, aged); + await Promise.all(Array.from({ length: 4 }, (_, i) => runWorker(rootDir, 'stale-worker-' + i))); + const records = loadCanonicalEvents(rootDir); + assert.equal(records.length, 49); + assert.equal(records.at(-1).sequence, 49); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + +test('T04-H16 canonical ledger hardlink cannot append to an external file', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const outsideRoot = tempProject(t); + const victim = path.join(outsideRoot, 'external-history.jsonl'); + const originalContent = fs.readFileSync(paths.events); + fs.writeFileSync(victim, originalContent); + fs.rmSync(paths.events); + try { + fs.linkSync(victim, paths.events); + } catch (e) { + if (['EPERM','EACCES','ENOTSUP','EXDEV'].includes(e.code)) { + t.skip('File hardlinks unavailable'); return; + } + throw e; + } + assert.throws(() => appendMetadataEvent({ rootDir, eventType: 'SHOULD_FAIL', payload: {} }), /hard.?link|multiple links/i); + assert.deepEqual(fs.readFileSync(victim), originalContent); +}); + +test('T04-H17 interrupted commit refreshes derived snapshot and index before clearing journal', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const before = fs.readFileSync(paths.events); + const oldSnapshot = fs.readFileSync(paths.snapshot); + const oldIndex = fs.readFileSync(paths.index); + appendMetadataEvent({ rootDir, eventType: 'FOLLOWUP_EVENT', payload: { value: 2 } }); + const after = fs.readFileSync(paths.events); + fs.writeFileSync(paths.snapshot, oldSnapshot); + fs.writeFileSync(paths.index, oldIndex); + fs.writeFileSync(paths.pending, JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: before.length, + previousHash: 'sha256:' + createHash('sha256').update(before).digest('hex'), + suffix: after.subarray(before.length).toString('utf8'), + })); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); + assert.equal(JSON.parse(fs.readFileSync(paths.snapshot, 'utf8')).lastEventSequence, 2); + assert.equal(JSON.parse(fs.readFileSync(paths.index, 'utf8')).sourceSequence, 2); + assert.equal(fs.existsSync(paths.pending), false); +}); + +test('T04-H18 stale reader cannot overwrite a later writer snapshot with old derived state', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const oldSnapshot = fs.readFileSync(paths.snapshot); + appendMetadataEvent({ rootDir, eventType: 'SECOND_EVENT', payload: { value: 2 } }); + fs.writeFileSync(paths.snapshot, oldSnapshot); + const originalRead = fs.readFileSync; + let insertedWriter = false; + fs.readFileSync = function staleReaderInterleave(file, ...args) { + if (!insertedWriter && file === paths.snapshot) { + insertedWriter = true; + const staleContent = originalRead.call(this, file, ...args); + appendMetadataEvent({ rootDir, eventType: 'THIRD_EVENT', payload: { value: 3 } }); + return staleContent; + } + return originalRead.call(this, file, ...args); + }; + try { + loadStateSnapshot(rootDir); + } finally { + fs.readFileSync = originalRead; + } + assert.equal(insertedWriter, true); + assert.equal(JSON.parse(fs.readFileSync(paths.snapshot, 'utf8')).lastEventSequence, 3); + assert.equal(JSON.parse(fs.readFileSync(paths.index, 'utf8')).sourceSequence, 3); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + + +test('T04-H22 event append refuses a symlink swapped into place immediately before open', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const outside = tempProject(t); + const victim = path.join(outside, 'must-not-change.txt'); + fs.writeFileSync(victim, 'EXTERNAL-SENTINEL', 'utf8'); + const originalOpen = fs.openSync; + let injected = false; + fs.openSync = function swapCanonicalPathAtAppendOpen(file, flags, ...args) { + const isAppend = flags === 'a' || + (typeof flags === 'number' && (flags & fs.constants.O_APPEND) !== 0); + const isTargetLedger = file === paths.events || + (typeof file === 'string' && file.includes('/proc/self/fd/') && file.endsWith('/events.jsonl')); + if (!injected && isTargetLedger && isAppend) { + const displaced = path.join(paths.stateRoot, 'original-ledger-for-test.jsonl'); + fs.renameSync(paths.events, displaced); + try { + fs.symlinkSync(victim, paths.events, 'file'); + } catch (err) { + fs.renameSync(displaced, paths.events); + if (['EPERM','EACCES','ENOTSUP'].includes(err.code)) { + t.skip('Host denies file symlink fixture'); + skipped = true; + return originalOpen.call(this, file, flags, ...args); + } + throw err; + } + injected = true; + } + return originalOpen.call(this, file, flags, ...args); + }; + let skipped = false; + try { + try { + appendMetadataEvent({ rootDir, eventType: 'MUST_NOT_ESCAPE', payload: {} }); + if (!skipped) { + assert.fail('Missing expected exception'); + } + } catch (err) { + if (skipped) return; + assert.match(String(err), /symlink|symbolic|inode|link|ELOOP|outside|State Engine/i); + } + } finally { + fs.openSync = originalOpen; + } + if (!injected || skipped) return; + assert.equal(fs.readFileSync(victim, 'utf8'), 'EXTERNAL-SENTINEL'); +}); + + +test('T04-H26 abandoned recovery guard is recovered only by explicit offline confirmation', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const guard = paths.lock + '.reclaim'; + fs.mkdirSync(guard, { mode: 0o700 }); + fs.writeFileSync(paths.lock, JSON.stringify({ + owner: 'dead-lock-owner', pid: 2147483647, + hostname: os.hostname(), acquiredAt: '2026-10-08T00:00:00.000Z', + })); + const ago = new Date(Date.now() - 120_000); + fs.utimesSync(paths.lock, ago, ago); + fs.utimesSync(guard, ago, ago); + assert.throws(() => recoverAbandonedStateLock({ rootDir }), /offline confirmation/i); + assert.equal(fs.existsSync(guard), true); + const recovered = recoverAbandonedStateLock({ rootDir, confirmOffline: true }); + assert.equal(recovered.recovered, true); + assert.equal(fs.existsSync(guard), false); + appendMetadataEvent({ rootDir, eventType: 'AFTER_ORPHAN_RECOVERY', payload: { value: 2 } }); + assert.equal(loadCanonicalEvents(rootDir).length, 2); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + +test('T04-H27 offline orphan guard recovery refuses a live owner', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const guard = paths.lock + '.reclaim'; + fs.mkdirSync(guard, { mode: 0o700 }); + fs.writeFileSync(path.join(guard, 'owner.json'), JSON.stringify({ + owner: 'still-running', pid: process.pid, hostname: os.hostname(), + })); + const ago = new Date(Date.now() - 120_000); + fs.utimesSync(guard, ago, ago); + assert.throws(() => recoverAbandonedStateLock({ rootDir, confirmOffline: true }), /live State Engine recovery guard/i); + assert.equal(fs.existsSync(guard), true); +}); + + +test('T04-H29 interrupted ledger recovery must never truncate an external hardlink swapped during read', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const prefix = fs.readFileSync(paths.events); + appendMetadataEvent({ rootDir, eventType: 'FOLLOWUP_EVENT', payload: { value: 2 } }); + const complete = fs.readFileSync(paths.events); + const suffix = complete.subarray(prefix.length); + fs.writeFileSync(paths.events, Buffer.concat([prefix, suffix.subarray(0, 40)])); + fs.writeFileSync(paths.pending, JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: prefix.length, + previousHash: 'sha256:' + createHash('sha256').update(prefix).digest('hex'), + suffix: suffix.toString('utf8'), + })); + const outside = tempProject(t); + const victim = path.join(outside, 'external-ledger.jsonl'); + fs.writeFileSync(victim, Buffer.concat([prefix, Buffer.alloc(500, 0x58)])); + const expectedExternal = fs.readFileSync(victim); + const originalRead = fs.readFileSync; + let swapped = false; + fs.readFileSync = function swapAfterLedgerRead(file, ...args) { + const bytes = originalRead.call(this, file, ...args); + if (!swapped && file === paths.events) { + swapped = true; + fs.renameSync(paths.events, path.join(paths.stateRoot, 'displaced-ledger')); + fs.linkSync(victim, paths.events); + } + return bytes; + }; + try { + // The corrected implementation may reject any inode substitution or + // complete on its original descriptor. Neither may corrupt the victim. + try { loadCanonicalEvents(rootDir); } catch (error) { + assert.match(String(error), /canonical|inode|link|history|State Engine/i); + } + } finally { + fs.readFileSync = originalRead; + } + assert.deepEqual(fs.readFileSync(victim), expectedExternal); +}); + + +test('T04-H36 pending journal cannot overwrite external file after state-directory swap', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'FIRST', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const outside = tempProject(t); + const protectedFile = path.join(outside, 'pending-commit.json'); + fs.writeFileSync(protectedFile, 'EXTERNAL-DATA-MUST-STAY', 'utf8'); + const moved = paths.stateRoot + '-held-for-test'; + const originalOpen = fs.openSync; + let swapped = false; + fs.openSync = function swapRootAtJournalTempOpen(target, flags, ...rest) { + if (!swapped && typeof target === 'string' && + target.includes('pending-commit.json.tmp-') && flags === 'wx') { + try { + fs.renameSync(paths.stateRoot, moved); + } catch (err) { + if (process.platform === 'win32' && err.code === 'EPERM') { + t.skip('Windows locks directory handles against renaming while pinned open'); + return originalOpen.call(this, target, flags, ...rest); + } + throw err; + } + try { + fs.symlinkSync(outside, paths.stateRoot, process.platform === 'win32' ? 'junction' : 'dir'); + } catch (error) { + fs.renameSync(moved, paths.stateRoot); + if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { + t.skip('Host does not permit test directory symlinks/junctions'); + return originalOpen.call(this, target, flags, ...rest); + } + throw error; + } + swapped = true; + } + return originalOpen.call(this, target, flags, ...rest); + }; + try { + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'SECOND_MUST_NOT_PUBLISH', payload: {} }), + /directory|symbolic|changed|outside|project root|symlink|mismatch/i, + ); + } finally { + fs.openSync = originalOpen; + if (swapped) { + fs.unlinkSync(paths.stateRoot); + fs.renameSync(moved, paths.stateRoot); + } + } + assert.equal(fs.readFileSync(protectedFile, 'utf8'), 'EXTERNAL-DATA-MUST-STAY'); + assert.equal(loadCanonicalEvents(rootDir).length, 1); +}); + +test('T04-H37 pending journal unlink fails closed when state directory is replaced during commit', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'FIRST', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const outside = tempProject(t); + const protectedFile = path.join(outside, 'pending-commit.json'); + fs.writeFileSync(protectedFile, 'EXTERNAL-SENTINEL-DO-NOT-DELETE', 'utf8'); + const moved = paths.stateRoot + '-held-for-unlink-test'; + const originalOpen = fs.openSync; + let swapped = false; + + // Intercept before unlink or atomic write + let journalCreated = false; + fs.openSync = function swapRootAtJournalUnlinkOpen(target, flags, ...rest) { + if (typeof target === 'string' && target.includes('pending-commit.json.tmp-')) { + journalCreated = true; + } + if (journalCreated && !swapped && typeof target === 'string' && (target.includes('snapshot.json') || target === paths.snapshot) && typeof flags === 'string' && flags.includes('w')) { + try { + fs.renameSync(paths.stateRoot, moved); + try { + fs.symlinkSync(outside, paths.stateRoot, process.platform === 'win32' ? 'junction' : 'dir'); + } catch (error) { + fs.renameSync(moved, paths.stateRoot); + if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { + t.skip('Host does not permit test directory symlinks/junctions'); + return originalOpen.call(this, target, flags, ...rest); + } + throw error; + } + swapped = true; + } catch (err) { + if (process.platform === 'win32' && err.code === 'EPERM') { + // Open directory descriptors prevent renaming the directory on Windows + t.skip('Windows locks directory handles against renaming while pinned open'); + return originalOpen.call(this, target, flags, ...rest); + } + throw err; + } + } + return originalOpen.call(this, target, flags, ...rest); + }; + + try { + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'SECOND_MUST_FAIL_CLEANUP', payload: {} }), + /directory|symbolic|changed|replaced|journal cleanup|mismatch/i, + ); + } finally { + fs.openSync = originalOpen; + if (swapped) { + fs.unlinkSync(paths.stateRoot); + fs.renameSync(moved, paths.stateRoot); + } + } + + assert.equal(fs.readFileSync(protectedFile, 'utf8'), 'EXTERNAL-SENTINEL-DO-NOT-DELETE', + 'External victim pending-commit.json must not be unlinked'); +}); + +test('T04-H38 pending journal unlink fails closed when state directory is swapped with an ordinary directory across transaction', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'FIRST', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const moved = paths.stateRoot + '-held-for-ordinary-dir-swap'; + const originalOpen = fs.openSync; + let swapped = false; + let journalCreated = false; + + fs.openSync = function swapRootWithOrdinaryDirectory(target, flags, ...rest) { + if (typeof target === 'string' && target.includes('pending-commit.json.tmp-')) { + journalCreated = true; + } + // Trigger swap after journal file is opened/written but before unlink + const isTargetLedger = target === paths.events || + (typeof target === 'string' && target.includes('/proc/self/fd/') && target.endsWith('/events.jsonl')); + if (journalCreated && !swapped && isTargetLedger && typeof flags === 'number') { + try { + fs.renameSync(paths.stateRoot, moved); + // Create a brand new ordinary directory at paths.stateRoot with a decoy pending-commit.json + fs.mkdirSync(paths.stateRoot, { recursive: true }); + fs.writeFileSync(paths.pending, 'DECOY-JOURNAL', 'utf8'); + swapped = true; + } catch (err) { + if (process.platform === 'win32' && err.code === 'EPERM') { + t.skip('Windows locks directory handles against renaming while pinned open'); + return originalOpen.call(this, target, flags, ...rest); + } + throw err; + } + } + return originalOpen.call(this, target, flags, ...rest); + }; + + try { + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'SECOND_MUST_FAIL_CLEANUP', payload: {} }), + /directory|changed|replaced/i, + ); + } finally { + fs.openSync = originalOpen; + if (swapped) { + fs.rmSync(paths.stateRoot, { recursive: true, force: true }); + fs.renameSync(moved, paths.stateRoot); + } + } +}); + +test('T04-H40 durable transaction writes fail closed when state directory is replaced before append or derived writes', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'FIRST', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const moved = paths.stateRoot + '-held-for-swap'; + const originalOpen = fs.openSync; + let swapped = false; + let journalCreated = false; + + fs.openSync = function swapRootBeforeAppend(target, flags, ...rest) { + if (typeof target === 'string' && target.includes('pending-commit.json.tmp-')) { + journalCreated = true; + } + // Swap state directory right before appendDurably opens paths.events + if (journalCreated && !swapped && (target === paths.events || (typeof target === 'string' && target.endsWith('/events.jsonl')))) { + try { + fs.renameSync(paths.stateRoot, moved); + fs.mkdirSync(paths.stateRoot, { recursive: true }); + fs.writeFileSync(paths.events, fs.readFileSync(path.join(moved, 'events.jsonl'))); + swapped = true; + } catch (err) { + if (process.platform === 'win32' && err.code === 'EPERM') { + t.skip('Windows locks directory handles against renaming while pinned open'); + return originalOpen.call(this, target, flags, ...rest); + } + throw err; + } + } + return originalOpen.call(this, target, flags, ...rest); + }; + + try { + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'SECOND_MUST_FAIL_APPEND', payload: {} }), + /directory|changed|replaced/i, + ); + } finally { + fs.openSync = originalOpen; + if (swapped) { + fs.rmSync(paths.stateRoot, { recursive: true, force: true }); + fs.renameSync(moved, paths.stateRoot); + } + } +}); + +test('T04-H41 recovery resolves pending journal, events, and snapshot relative to pinned directory descriptor', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const before = fs.readFileSync(paths.events, 'utf8'); + appendMetadataEvent({ rootDir, eventType: 'FOLLOWUP_EVENT', payload: { value: 2 } }); + const expected = fs.readFileSync(paths.events, 'utf8'); + const suffix = expected.slice(before.length); + // Simulate torn append with pending journal + fs.writeFileSync(paths.events, before + suffix.slice(0, 10), 'utf8'); + fs.writeFileSync(paths.pending, JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(before), + previousHash: 'sha256:' + createHash('sha256').update(before).digest('hex'), + suffix, + }), 'utf8'); + + const openedTargets = []; + const originalOpen = fs.openSync; + fs.openSync = function trackOpen(target, flags, ...rest) { + if (typeof target === 'string') { + openedTargets.push(target); + } + return originalOpen.call(this, target, flags, ...rest); + }; + + try { + const recovered = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); + assert.ok(recovered); + assert.equal(recovered.lastEventSequence, 2); + if (process.platform === 'linux') { + const procFdEvents = openedTargets.find(p => p.includes('/proc/self/fd/') && p.endsWith('events.jsonl')); + assert.ok(procFdEvents, 'Linux recovery must open events ledger through /proc/self/fd/ descriptor path'); + } + } finally { + fs.openSync = originalOpen; + } +}); + +test('T04-H42 appendDurably fails closed if canonical ledger entry is displaced during write', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'INITIAL_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const displaced = path.join(paths.stateRoot, 'displaced-events.jsonl'); + + const originalWriteSync = fs.writeSync; + let swapped = false; + fs.writeSync = function hookedWrite(fd, buffer, offset, length, position) { + if (!swapped) { + try { + fs.renameSync(paths.events, displaced); + fs.writeFileSync(paths.events, 'MUTATED_NEW_FILE\n', 'utf8'); + swapped = true; + } catch (err) { + if (process.platform === 'win32' && err.code === 'EPERM') { + t.skip('Windows locks open files against renaming'); + return originalWriteSync.call(this, fd, buffer, offset, length, position); + } + throw err; + } + } + return originalWriteSync.call(this, fd, buffer, offset, length, position); + }; + + try { + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'MUST_FAIL_EVENT', payload: { value: 2 } }), + /displaced|changed|mismatch/i, + ); + } finally { + fs.writeSync = originalWriteSync; + if (swapped) { + fs.rmSync(paths.events, { force: true }); + fs.renameSync(displaced, paths.events); + } + } +}); + + + + +function swapLedgerBeforeJournalUnlink(t, paths, replacementContent) { + const originalUnlink = fs.unlinkSync; + const displaced = path.join(paths.stateRoot, 'displaced-events.jsonl'); + const state = { swapped: false, skipped: false, displaced, restore() { + fs.unlinkSync = originalUnlink; + } }; + fs.unlinkSync = function swapBetweenFinalLstatAndUnlink(target, ...rest) { + if (!state.swapped && typeof target === 'string' && target.endsWith('pending-commit.json')) { + try { + fs.renameSync(paths.events, displaced); + fs.writeFileSync(paths.events, replacementContent, 'utf8'); + state.swapped = true; + } catch (err) { + if (process.platform === 'win32' && ['EPERM', 'EBUSY', 'EACCES'].includes(err.code)) { + state.skipped = true; + t.skip('Windows denies renaming the open ledger; Ubuntu CI provides the inode-swap evidence'); + } else { + throw err; + } + } + } + return originalUnlink.call(this, target, ...rest); + }; + return state; +} + +test('T04-H43 ledger swap between final lstat and journal unlink is detected fail-closed on next load (append)', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const preLedger = fs.readFileSync(paths.events, 'utf8'); + const state = swapLedgerBeforeJournalUnlink(t, paths, preLedger); + try { + appendMetadataEvent({ rootDir, eventType: 'AMBIGUOUS_EVENT', payload: { value: 2 } }); + } finally { + state.restore(); + } + if (state.skipped) return; + assert.equal(state.swapped, true); + // Post-condition witnesses: canonical authority is the replaced ledger left directly in place by the hook, + // the pending journal was removed, and derived state is ahead of the replaced ledger. + assert.equal(fs.existsSync(paths.pending), false); + assert.equal(fs.existsSync(state.displaced), true); + assert.equal(fs.readFileSync(state.displaced, 'utf8').split('\n').filter(Boolean).length, 2, + 'displaced ledger holds both committed events'); + assert.equal(fs.readFileSync(paths.events, 'utf8'), preLedger, + 'events.jsonl contains the authentic swapped preLedger without synthetic post-operation rewrite'); + assert.equal(loadCanonicalEvents(rootDir).length, 1); + assert.throws(() => loadStateSnapshot(rootDir, { rebuildIfNeeded: false }), /truncated|differs|materialized/i); + assert.throws(() => verifyStateEngineIntegrity(rootDir), /truncated|differs|materialized|equivalent/i); +}); + +test('T04-H44 ledger swap between final lstat and journal unlink is detected fail-closed on next load (recovery)', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const before = fs.readFileSync(paths.events, 'utf8'); + appendMetadataEvent({ rootDir, eventType: 'FOLLOWUP_EVENT', payload: { value: 2 } }); + const suffix = fs.readFileSync(paths.events, 'utf8').slice(before.length); + fs.writeFileSync(paths.events, before + suffix.slice(0, 10), 'utf8'); + fs.writeFileSync(paths.pending, JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(before), + previousHash: 'sha256:' + createHash('sha256').update(before).digest('hex'), + suffix, + }), 'utf8'); + const state = swapLedgerBeforeJournalUnlink(t, paths, before); + try { + assert.throws(() => loadStateSnapshot(rootDir, { rebuildIfNeeded: true }), /truncated|differs|materialized/i); + } finally { + state.restore(); + } + if (state.skipped) return; + assert.equal(state.swapped, true); + assert.equal(fs.existsSync(paths.pending), false); + assert.equal(fs.existsSync(state.displaced), true); + assert.equal(fs.readFileSync(paths.events, 'utf8'), before, + 'events.jsonl contains the authentic swapped before ledger without synthetic rewrite'); + assert.throws(() => loadStateSnapshot(rootDir, { rebuildIfNeeded: false }), /truncated|differs|materialized/i); + assert.throws(() => verifyStateEngineIntegrity(rootDir), /truncated|differs|materialized|equivalent/i); +}); + +test('T04-H45 missing events.jsonl with pending journal present fails closed across all accessors', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + + // Setup pending commit marker + fs.writeFileSync(paths.pending, JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: 0, + previousHash: 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + suffix: '{"schemaVersion":"1.0.0"}\n', + }), 'utf8'); + + // Remove the canonical events ledger + fs.unlinkSync(paths.events); + + // Verify loadCanonicalEvents fails closed + assert.throws( + () => loadCanonicalEvents(rootDir), + /events\.jsonl is missing while pending-commit\.json is present/i, + ); + + // Verify loadStateSnapshot fails closed + assert.throws( + () => loadStateSnapshot(rootDir, { rebuildIfNeeded: false }), + /events\.jsonl is missing while pending-commit\.json is present/i, + ); + + // Verify ensureStateEngineLayout fails closed rather than wiping/creating an empty ledger + assert.throws( + () => ensureStateEngineLayout(rootDir), + /events\.jsonl is missing while pending-commit\.json is present/i, + ); + + // Verify verifyStateEngineIntegrity fails closed + assert.throws( + () => verifyStateEngineIntegrity(rootDir), + /events\.jsonl is missing while pending-commit\.json is present/i, + ); +}); + +test('T04-H46 recovery fsyncs a fully present but unflushed suffix before acknowledging journal', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H46_INITIAL', payload: { seq: 1 } }); + const paths = getStateEnginePaths(rootDir); + const before = fs.readFileSync(paths.events, 'utf8'); + appendMetadataEvent({ rootDir, eventType: 'H46_FOLLOWUP', payload: { seq: 2 } }); + const full = fs.readFileSync(paths.events, 'utf8'); + const suffix = full.slice(before.length); + // Writer died after writing the complete suffix, before fsync and journal removal. + fs.writeFileSync(paths.pending, JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(before), + previousHash: 'sha256:' + createHash('sha256').update(before).digest('hex'), + suffix, + }), 'utf8'); + const ledger = fs.statSync(paths.events); + const originalFsync = fs.fsyncSync; + const originalUnlink = fs.unlinkSync; + let ledgerFlushed = false; + let observedJournalRemoval = false; + fs.fsyncSync = function trackLedgerFsync(fd) { + const stat = fs.fstatSync(fd); + if (stat.isFile() && stat.dev === ledger.dev && stat.ino === ledger.ino) ledgerFlushed = true; + return originalFsync.call(this, fd); + }; + fs.unlinkSync = function guardJournalUnlink(target, ...args) { + if (typeof target === 'string' && target.endsWith('pending-commit.json')) { + observedJournalRemoval = true; + assert.ok(ledgerFlushed, + 'Recovery must fsync fully present canonical suffix before removing pending journal'); + } + return originalUnlink.call(this, target, ...args); + }; + try { + assert.equal(loadStateSnapshot(rootDir, { rebuildIfNeeded: true }).lastEventSequence, 2); + assert.ok(observedJournalRemoval); + assert.equal(fs.existsSync(paths.pending), false); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); + } finally { + fs.fsyncSync = originalFsync; + fs.unlinkSync = originalUnlink; + } +}); + + +test('T04-H49 Windows refuses canonical ledger mutation when pending-journal directory flush fails', (t) => { + if (process.platform !== 'win32') { + t.skip('Windows write-capable directory flush barrier'); + return; + } + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H49_BASELINE', payload: { step: 1 } }); + const paths = getStateEnginePaths(rootDir); + const before = fs.readFileSync(paths.events); + const dir = fs.statSync(paths.stateRoot); + const originalFsync = fs.fsyncSync; + let injected = false; + fs.fsyncSync = function failStateDirectory(fd) { + const witness = fs.fstatSync(fd); + if (witness.isDirectory() && witness.dev === dir.dev && witness.ino === dir.ino) { + injected = true; + const error = new Error('injected Windows namespace durability failure'); + error.code = 'EIO'; + throw error; + } + return originalFsync.call(this, fd); + }; + try { + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'H49_FOLLOWUP', payload: { step: 2 } }), + /directory durability barrier failed/i, + ); + } finally { + fs.fsyncSync = originalFsync; + } + assert.equal(injected, true); + assert.deepEqual(fs.readFileSync(paths.events), before, + 'The ledger must not be changed if publishing the pending journal is not durable'); + assert.equal(fs.existsSync(paths.pending), true, + 'Keep recovery evidence when pending journal namespace cannot be flushed'); +}); + +test('T04-H50 Windows flushes pending-journal directory before any physical canonical append', (t) => { + if (process.platform !== 'win32') { + t.skip('Windows pending-journal namespace ordering'); + return; + } + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H50_BASELINE', payload: { step: 1 } }); + const paths = getStateEnginePaths(rootDir); + const dir = fs.statSync(paths.stateRoot); + const ledger = fs.statSync(paths.events); + const originalFsync = fs.fsyncSync; + const originalWrite = fs.writeSync; + let namespaceFlushed = false; + let sawLedgerAppend = false; + fs.fsyncSync = function watchStateDirectoryFlush(fd) { + const stat = fs.fstatSync(fd); + const returned = originalFsync.call(this, fd); + if (stat.isDirectory() && stat.dev === dir.dev && stat.ino === dir.ino) { + namespaceFlushed = true; + } + return returned; + }; + fs.writeSync = function guardLedgerAppend(fd, ...args) { + const stat = fs.fstatSync(fd); + if (stat.isFile() && stat.dev === ledger.dev && stat.ino === ledger.ino) { + sawLedgerAppend = true; + assert.equal(namespaceFlushed, true, + 'The pending journal must be flushed in its parent before appending ledger bytes'); + } + return originalWrite.call(this, fd, ...args); + }; + try { + appendMetadataEvent({ rootDir, eventType: 'H50_FOLLOWUP', payload: { step: 2 } }); + } finally { + fs.fsyncSync = originalFsync; + fs.writeSync = originalWrite; + } + assert.equal(sawLedgerAppend, true); + assert.equal(fs.existsSync(paths.pending), false); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From 3e85dc1e403b667ee154624855de1c5355f1f709 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:29:09 +0200 Subject: [PATCH 011/177] test(v0.13): port T04 candidate state-engine-v2-benchmark.mjs --- scripts/state-engine-v2-benchmark.mjs | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 scripts/state-engine-v2-benchmark.mjs diff --git a/scripts/state-engine-v2-benchmark.mjs b/scripts/state-engine-v2-benchmark.mjs new file mode 100644 index 000000000..3161c8275 --- /dev/null +++ b/scripts/state-engine-v2-benchmark.mjs @@ -0,0 +1,60 @@ +// Deterministic, disposable T04 endurance fixture. Records observed performance; +// it does not turn a host-specific elapsed time into a false release acceptance. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { performance } from 'node:perf_hooks'; +import { + appendEntityState, + getStateEnginePaths, + loadCanonicalEvents, + loadEntityState, + verifyStateEngineIntegrity, +} from '../runtime/orchestration/state-engine-v2.mjs'; + +const countArg = process.argv.includes('--events') + ? Number(process.argv[process.argv.indexOf('--events') + 1]) + : 300; +assert.ok(Number.isInteger(countArg) && countArg >= 1 && countArg <= 5000, '--events must be between 1 and 5000'); +const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dk-t04-endurance-')); +try { + const start = performance.now(); + let modeledOldRewriteBytes = 0; + for (let revision = 1; revision <= countArg; revision += 1) { + appendEntityState({ + rootDir, + entityType: 'endurance-fixture', + entityId: 'same-entity', + state: { stateRevision: revision, value: revision, mark: 'T04' }, + actorClass: 'endurance-test', + timestamp: new Date(Date.UTC(2026, 9, 9, 0, 0, 0) + revision * 1000).toISOString(), + }); + modeledOldRewriteBytes += fs.statSync(getStateEnginePaths(rootDir).events).size; + } + const writeDurationMs = performance.now() - start; + const readStart = performance.now(); + const events = loadCanonicalEvents(rootDir); + const state = loadEntityState('endurance-fixture', 'same-entity', rootDir); + const integrity = verifyStateEngineIntegrity(rootDir); + const readVerifyDurationMs = performance.now() - readStart; + assert.equal(events.length, countArg); + assert.equal(state.stateRevision, countArg); + assert.equal(integrity.valid, true); + const bytes = fs.statSync(getStateEnginePaths(rootDir).events).size; + console.log('T04_ENDURANCE_EVIDENCE ' + JSON.stringify({ + events: countArg, + platform: process.platform, + node: process.version, + writeDurationMs: Math.round(writeDurationMs), + readVerifyDurationMs: Math.round(readVerifyDurationMs), + canonicalLedgerBytes: bytes, + physicallyAppendedEventBytes: bytes, + historicalFullRewriteByteModel: modeledOldRewriteBytes, + rewriteBytesAvoidedPercentModeled: Number(((1 - bytes / modeledOldRewriteBytes) * 100).toFixed(2)), + pass: integrity.valid, + limitations: 'Historical full-rewrite traffic is computed from file sizes, not a measured legacy run. CPU parsing/replay is still full-history.', + })); +} finally { + fs.rmSync(rootDir, { recursive: true, force: true }); +} From 11ec6ec134f766e2e7f8ce11c2fb531f06f36035 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:29:14 +0200 Subject: [PATCH 012/177] test(v0.13): port T04 candidate t04-review-thread-blockers.mjs --- scripts/t04-review-thread-blockers.mjs | 121 +++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 scripts/t04-review-thread-blockers.mjs diff --git a/scripts/t04-review-thread-blockers.mjs b/scripts/t04-review-thread-blockers.mjs new file mode 100644 index 000000000..cb788183d --- /dev/null +++ b/scripts/t04-review-thread-blockers.mjs @@ -0,0 +1,121 @@ +// Deterministic review-thread blocker selection for T04 risk-3 acceptance. +// A requested clean review requires zero unresolved comments of any priority. +export function unresolvedReviewThreads(threads) { + if (!Array.isArray(threads)) throw new TypeError('Review thread evidence must be an array'); + return threads.filter(thread => !thread || thread.isResolved !== true); +} + +export function requireCanonicalReviewRequestCommit(body, reviewedCommit) { + if (typeof body !== 'string' || !/^[a-f0-9]{40}$/.test(reviewedCommit)) { + throw new Error('Review request commit evidence is malformed'); + } + // Exactly one unambiguous source-binding field is permitted. An authenticated + // reaction alone has no Git object provenance; ambiguous requests fail closed. + const fields = [...body.matchAll(/^(?:Commit|Immutable source commit):[ \t]*([a-f0-9]{40})[ \t]*$/gm)]; + // Conservatively reject every other plausible Git abbreviation, including + // uppercase identifiers. Hex-only prose may therefore need rewording. + const hashes = body.match(/\b[a-f0-9]{7,40}\b/gi) ?? []; + if (fields.length !== 1 || fields[0][1] !== reviewedCommit || + hashes.length !== 1 || hashes[0] !== reviewedCommit) { + throw new Error('Reviewer scope request must identify exactly one canonical reviewed commit'); + } + return reviewedCommit; +} + +export function parseReviewThreadPage(page) { + if (!page || !Array.isArray(page.nodes) || !page.pageInfo || + typeof page.pageInfo.hasNextPage !== 'boolean' || + (page.pageInfo.endCursor != null && typeof page.pageInfo.endCursor !== 'string') || + (page.pageInfo.hasNextPage && + (typeof page.pageInfo.endCursor !== 'string' || !page.pageInfo.endCursor)) || + page.nodes.some(node => !node || typeof node.isResolved !== 'boolean')) { + throw new Error('Authenticated GitHub review thread page is malformed'); + } + return { + nodes: page.nodes, + hasNextPage: page.pageInfo.hasNextPage, + endCursor: page.pageInfo.endCursor ?? null, + }; +} + +/** + * A COMMENTED review is not a clean attestation just because negative prose + * contains the word PASS. Require an explicit verdict without contradictions. + */ +export function requireCanonicalGithubReviewVerdict(body) { + if (typeof body !== 'string') throw new Error('GitHub reviewer verdict evidence is malformed'); + // Closed schema: no trailers, markdown prose, second findings field or + // unstructured text capable of contradicting a declared clean review. + const lines = body.replace(/\r\n/g, '\n').split('\n'); + if (lines.length === 1) { + throw new Error('Independent GitHub review requires exactly one canonical PASS verdict'); + } + if (lines.length !== 2 && lines.length !== 4) { + throw new Error('Conflicting failure or finding marker in GitHub review: unexpected response fields'); + } + const [verdict, findings] = lines.slice(-2); + if (!/^(?:Review verdict|Verdict): PASS$/.test(verdict)) { + throw new Error('Independent GitHub review requires exactly one canonical PASS verdict'); + } + if (findings !== 'Findings: none') { + throw new Error('Conflicting failure or finding marker in GitHub review: findings not clean'); + } + if (lines.length === 4 && + (!/^Reviewed commit: [a-f0-9]{40}$/.test(lines[0]) || + !/^Review role: (?:architecture-reviewer|code-reviewer|security-reviewer)$/.test(lines[1]))) { + throw new Error('Conflicting failure or finding marker in GitHub review: noncanonical source or role'); + } + return 'PASS'; +} + +/** + * Construct a single-role immutable review request with exactly one SHA. + * The result must still be independently authored and evaluated externally. + */ +export function formatCanonicalT04ReviewRequest(role, reviewedCommit, scope) { + if (!['architecture-reviewer', 'code-reviewer', 'security-reviewer'].includes(role) || + typeof scope !== 'string' || !scope.trim() || /[\r\n]/.test(scope)) { + throw new Error('T04 reviewer request role/scope is invalid'); + } + const body = [ + '@codex review', + 'Independent role: ' + role, + 'Commit: ' + reviewedCommit, + 'Review scope: ' + scope, + 'Report findings with priority; use the exact closed four-field response schema on a clean result. A finding must not be concealed by a PASS.', + ].join('\n'); + requireCanonicalReviewRequestCommit(body, reviewedCommit); + return body; +} + +function requireCanonicalRole(body, label, expectedRole) { + const roles = ['architecture-reviewer', 'code-reviewer', 'security-reviewer']; + const fields = [...body.matchAll(new RegExp('^' + label + ':[ \\t]*(.*)$', 'gmi'))]; + if (!roles.includes(expectedRole) || fields.length !== 1 || + fields[0][0] !== label + ': ' + expectedRole) { + throw new Error('Reviewer evidence requires exactly one canonical ' + label); + } +} + +export function requireCanonicalGithubReviewRequest(request, expectedRole, reviewedCommit) { + if (!request || request.issue_url !== 'https://api.github.com/repos/eybersjp/development-kit/issues/70' || + typeof request.body !== 'string' || !request.body.includes('@codex')) { + throw new Error('Authenticated reviewer scope request must bind PR #70'); + } + requireCanonicalRole(request.body, 'Independent role', expectedRole); + requireCanonicalReviewRequestCommit(request.body, reviewedCommit); + return reviewedCommit; +} + +export function requireCanonicalGithubReviewResponse(review, expectedRole, reviewedCommit) { + if (!review || review.commit_id !== reviewedCommit || typeof review.body !== 'string') { + throw new Error('Authenticated review metadata must bind the immutable reviewed source'); + } + requireCanonicalRole(review.body, 'Review role', expectedRole); + // Reuse the same conservative source-token policy with a response-only label. + if (/^(?:Commit|Immutable source commit):/mi.test(review.body)) { + throw new Error('Review response contains an alternative source-binding field'); + } + requireCanonicalReviewRequestCommit(review.body.replace(/^Reviewed commit:/gm, 'Commit:'), reviewedCommit); + return requireCanonicalGithubReviewVerdict(review.body); +} From cb6bd4228de33830c293eb54446a5c2e696b5220 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:29:17 +0200 Subject: [PATCH 013/177] test(v0.13): port T04 candidate t04-review-thread-blockers.test.mjs --- scripts/t04-review-thread-blockers.test.mjs | 176 ++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 scripts/t04-review-thread-blockers.test.mjs diff --git a/scripts/t04-review-thread-blockers.test.mjs b/scripts/t04-review-thread-blockers.test.mjs new file mode 100644 index 000000000..dd80e63b9 --- /dev/null +++ b/scripts/t04-review-thread-blockers.test.mjs @@ -0,0 +1,176 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import * as reviewParsers from './t04-review-thread-blockers.mjs'; +import { + unresolvedReviewThreads, + requireCanonicalReviewRequestCommit, + parseReviewThreadPage, + requireCanonicalGithubReviewVerdict, + formatCanonicalT04ReviewRequest, +} from './t04-review-thread-blockers.mjs'; + +test('T04-A01 unresolved P2 and all other finding priorities prevent acceptance', () => { + const p2 = { id: 'p2', isResolved: false, comments: { nodes: [{ body: '[P2] broken validator' }] } }; + const p1 = { id: 'p1', isResolved: false, comments: { nodes: [{ body: '[P1] lost data' }] } }; + const minor = { id: 'minor', isResolved: false, comments: { nodes: [{ body: 'Unanswered question' }] } }; + assert.deepEqual(unresolvedReviewThreads([p2, p1, minor]).map(x => x.id), ['p2', 'p1', 'minor']); +}); + +test('RI-01 source binding rejects abbreviated and uppercase alternate identifiers', () => { + const source = 'a'.repeat(40); + for (const alternate of ['bbbbbbb', 'B'.repeat(40), source.toUpperCase(), source.slice(0, 10)]) { + assert.throws(() => requireCanonicalReviewRequestCommit('Commit: ' + source + '\nBaseline: ' + alternate, source)); + } +}); + +test('RI-01 clean verdict requires one canonical findings declaration and no structured severity', () => { + for (const findings of ['', 'Findings: MAJOR Windows journal publication may lose data', + 'Findings: none\nSeverity: CRITICAL', 'Findings: none\nStatus: OPEN', + 'Findings: none\nFindings: none', 'Findings: none\n[MAJOR] data loss']) { + assert.throws(() => requireCanonicalGithubReviewVerdict('Verdict: PASS\n' + findings)); + } +}); + +test('RI-02 authenticated scope request binds PR and exactly one role', () => { + const source = 'a'.repeat(40); + const request = { issue_url: 'https://api.github.com/repos/eybersjp/development-kit/issues/70', + body: '@codex\nIndependent role: code-reviewer\nCommit: ' + source }; + assert.equal(typeof reviewParsers.requireCanonicalGithubReviewRequest, 'function'); + assert.doesNotThrow(() => reviewParsers.requireCanonicalGithubReviewRequest(request, 'code-reviewer', source)); + for (const invalid of [{ ...request, issue_url: request.issue_url.replace('/70', '/71') }, + { ...request, issue_url: undefined }, + { ...request, body: request.body + '\nIndependent role: code-reviewer' }, + { ...request, body: request.body + '\nIndependent role: security-reviewer' }]) { + assert.throws(() => reviewParsers.requireCanonicalGithubReviewRequest(invalid, 'code-reviewer', source)); + } +}); + +test('RI-01 authenticated response binds unique anchored role and full API source', () => { + const source = 'a'.repeat(40); + const review = { commit_id: source, body: 'Reviewed commit: ' + source + + '\nReview role: code-reviewer\nVerdict: PASS\nFindings: none' }; + assert.equal(typeof reviewParsers.requireCanonicalGithubReviewResponse, 'function'); + assert.doesNotThrow(() => reviewParsers.requireCanonicalGithubReviewResponse(review, 'code-reviewer', source)); + for (const invalid of [{ ...review, commit_id: 'b'.repeat(40) }, + { ...review, body: review.body.replace(source, source.slice(0, 10)) }, + { ...review, body: review.body + '\nReview role: code-reviewer' }, + { ...review, body: review.body + '\nReview role: security-reviewer' }, + { ...review, body: review.body.replace('Review role:', 'Example Review role:') }, + { ...review, body: review.body + '\nBaseline: bbbbbbb' }]) { + assert.throws(() => reviewParsers.requireCanonicalGithubReviewResponse(invalid, 'code-reviewer', source)); + } +}); + +test('RI-01 final pagination cursor rejects malformed types', () => { + for (const endCursor of [123, {}, []]) { + assert.throws(() => parseReviewThreadPage({ nodes: [], pageInfo: { hasNextPage: false, endCursor } })); + } +}); + +test('RI-01 clean declaration cannot suppress a separate structured minor finding', () => { + for (const marker of ['Severity: MINOR', 'Finding: data loss', '[P3] unanswered question', 'Status: PENDING']) { + assert.throws(() => requireCanonicalGithubReviewVerdict('Verdict: PASS\nFindings: none\n' + marker)); + } +}); + +test('T04-A02 resolved threads cannot block a clean exact-source review', () => { + assert.deepEqual(unresolvedReviewThreads([{ id: 'p2', isResolved: true }, { id: 'p1', isResolved: true }]), []); +}); + +test('T04-A03 malformed or uncertain review thread state fails closed', () => { + assert.equal(unresolvedReviewThreads([{ id: 'missing-resolution' }, null]).length, 2); + assert.throws(() => unresolvedReviewThreads(null), /must be an array/i); +}); + +test('T04-A04 reviewer clean reaction requires one unambiguous anchored commit field', () => { + const current = 'a'.repeat(40); + const other = 'b'.repeat(40); + assert.equal(requireCanonicalReviewRequestCommit('Commit: ' + current, current), current); + assert.equal(requireCanonicalReviewRequestCommit('Immutable source commit: ' + current, current), current); + assert.throws(() => requireCanonicalReviewRequestCommit( + 'Commit: ' + other + '\nReference baseline: ' + current, current, + ), /exactly one canonical/i); + assert.throws(() => requireCanonicalReviewRequestCommit( + 'Commit: ' + current + '\nIgnore other commit ' + other, current, + ), /exactly one canonical/i); + assert.throws(() => requireCanonicalReviewRequestCommit( + 'Commit: ' + current + '\nCommit: ' + current, current, + ), /exactly one canonical/i); + assert.throws(() => requireCanonicalReviewRequestCommit( + 'Exclude: ' + current + '\nCommit: ' + other, current, + ), /exactly one canonical/i); + assert.throws(() => requireCanonicalReviewRequestCommit( + 'context mentions ' + current, current, + ), /exactly one canonical/i); +}); + +test('T04-A05 authenticated GraphQL thread pages fail closed on missing nodes or pagination', () => { + const good = { nodes: [{ id: 'thread', isResolved: false }], + pageInfo: { hasNextPage: false, endCursor: null } }; + assert.deepEqual(parseReviewThreadPage(good).nodes, good.nodes); + assert.deepEqual(parseReviewThreadPage({ nodes: [], + pageInfo: { hasNextPage: true, endCursor: 'cursor-1' } }).endCursor, 'cursor-1'); + assert.throws(() => parseReviewThreadPage({ ...good, nodes: null }), /malformed/i); + assert.throws(() => parseReviewThreadPage({ ...good, pageInfo: null }), /malformed/i); + assert.throws(() => parseReviewThreadPage({ ...good, + pageInfo: { hasNextPage: true, endCursor: null } }), /malformed/i); + assert.throws(() => parseReviewThreadPage({ ...good, nodes: [null] }), /malformed/i); + assert.throws(() => parseReviewThreadPage({ ...good, + nodes: [{ id: 'unknown' }] }), /malformed/i); +}); + +test('T04-A06 COMMENTED review requires a canonical PASS without conflicting findings', () => { + const valid = 'Reviewed commit: ' + 'a'.repeat(40) + + '\nReview role: code-reviewer\nReview verdict: PASS\nFindings: none'; + assert.equal(requireCanonicalGithubReviewVerdict(valid), 'PASS'); + for (const body of [ + 'cannot PASS', + 'Review role: code-reviewer\nCannot PASS until P1 fixed', + 'Verdict: PASS\nCannot PASS until the durability issue is fixed', + 'Verdict: PASS\nP1 Windows crash durability still open', + 'Verdict: PASS\nVerdict: FAIL', + 'Verdict: FAIL', + 'Verdict: PASS\nReview verdict: PASS', + 'Verdict: PASS\nBlocked by unresolved review', + 'Verdict: PASS\nnot PASS', + ]) { + assert.throws(() => requireCanonicalGithubReviewVerdict(body), + /canonical PASS verdict|Conflicting failure or finding marker/i, body); + } +}); + +test('T04-A07 reject extra free-text contradicting zero-findings review', () => { + const full = 'Reviewed commit: ' + 'a'.repeat(40) + + '\nReview role: code-reviewer\nVerdict: PASS\nFindings: none'; + assert.equal(reviewParsers.requireCanonicalGithubReviewResponse({ + commit_id: 'a'.repeat(40), body: full, + }, 'code-reviewer', 'a'.repeat(40)), 'PASS'); + for (const additional of [ + 'The restore loses the original tree after a power outage', + 'Possible loss of user data on Windows', + 'Further investigation is required', + 'Verdict: PASS', + 'Findings: none', + '', + ]) { + assert.throws(() => reviewParsers.requireCanonicalGithubReviewResponse({ + commit_id: 'a'.repeat(40), body: full + '\n' + additional, + }, 'code-reviewer', 'a'.repeat(40)), /Conflicting failure|canonical/i); + } +}); + +test('T04-A08 generated reviewer request has one immutable SHA and exactly one role', () => { + const source = 'a'.repeat(40); + const roles = ['code-reviewer', 'architecture-reviewer', 'security-reviewer']; + for (const role of roles) { + const body = formatCanonicalT04ReviewRequest(role, source, 'Inspect precise T04 source and evidence.'); + assert.equal((body.match(new RegExp(source, 'g')) || []).length, 1); + assert.equal(reviewParsers.requireCanonicalGithubReviewRequest({ + issue_url: 'https://api.github.com/repos/eybersjp/development-kit/issues/70', + body, + }, role, source), source); + assert.throws(() => formatCanonicalT04ReviewRequest( + role, source, 'Inspect a different revision ' + 'b'.repeat(40) + '.', + ), /one canonical reviewed commit/i); + } +}); From 152a0bd06cd909ed514f45c864da1c27474f3377 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:29:31 +0200 Subject: [PATCH 014/177] feat(v0.13): route Autopilot state through V2 after verified cutover --- runtime/autopilot/state-store.mjs | 107 ++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 22 deletions(-) diff --git a/runtime/autopilot/state-store.mjs b/runtime/autopilot/state-store.mjs index 2c6e31e5f..03c4e0753 100644 --- a/runtime/autopilot/state-store.mjs +++ b/runtime/autopilot/state-store.mjs @@ -1,14 +1,26 @@ /** - * Development Kit Autopilot — Immutable Snapshot State Store + * Development Kit Autopilot — State Store * - * Implements crash-consistent versioned snapshot persistence: - * .development-kit/autopilot/state/revision-XXXXXX.json & current.json. + * State Engine V2 is canonical for: + * - fresh projects with no legacy Autopilot state chain; + * - legacy projects after their workflow has been migrated into V2. + * + * Unmigrated legacy projects continue to use the legacy immutable-snapshot store + * until explicit migration succeeds. This prevents silent history loss. */ import fs from 'node:fs'; import path from 'node:path'; + import { validateWorkflowState } from './validators.mjs'; import { acquireTransactionLock, releaseTransactionLock } from './lock-manager.mjs'; +import { + appendEntityState, + hasCompletedStateMigration, + isStateEngineV2Active, + loadLatestEntityState, + rebuildStateSnapshot, +} from '../orchestration/state-engine-v2.mjs'; export class StateStoreError extends Error { constructor(message) { @@ -21,13 +33,27 @@ export function getStateDir(rootDir = process.cwd()) { return path.join(rootDir, '.development-kit', 'autopilot', 'state'); } -export function getCurrentState(rootDir = process.cwd()) { +function legacyStateExists(rootDir) { + const stateDir = getStateDir(rootDir); + if (!fs.existsSync(stateDir)) return false; + return fs.readdirSync(stateDir).some((name) => /^revision-\d{6}\.json$/.test(name)); +} + +function migratedWorkflowState(rootDir) { + if (!isStateEngineV2Active(rootDir)) return null; + return loadLatestEntityState('autopilot-workflow', rootDir); +} + +function shouldUseV2(rootDir) { + if (!legacyStateExists(rootDir)) return true; + return hasCompletedStateMigration(rootDir) && migratedWorkflowState(rootDir) !== null; +} + +function getLegacyCurrentState(rootDir) { const stateDir = getStateDir(rootDir); const currentFile = path.join(stateDir, 'current.json'); - if (!fs.existsSync(currentFile)) { - return null; - } + if (!fs.existsSync(currentFile)) return null; try { const pointer = JSON.parse(fs.readFileSync(currentFile, 'utf8')); @@ -38,18 +64,27 @@ export function getCurrentState(rootDir = process.cwd()) { return state; } } catch { - // Pointer or state file corrupt — attempt recovery + // Pointer or state file corrupt — attempt legacy recovery. } - return recoverLatestValidState(rootDir); + return recoverLatestLegacyState(rootDir); } -export function saveStateRevision(state, rootDir = process.cwd()) { - validateWorkflowState(state); - const stateDir = getStateDir(rootDir); - if (!fs.existsSync(stateDir)) { - fs.mkdirSync(stateDir, { recursive: true }); +export function getCurrentState(rootDir = process.cwd()) { + if (shouldUseV2(rootDir)) { + const state = migratedWorkflowState(rootDir); + if (state) { + validateWorkflowState(state); + return state; + } + if (!legacyStateExists(rootDir)) return null; } + return getLegacyCurrentState(rootDir); +} + +function saveLegacyStateRevision(state, rootDir) { + const stateDir = getStateDir(rootDir); + if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true }); const lock = acquireTransactionLock(rootDir); try { @@ -63,7 +98,6 @@ export function saveStateRevision(state, rootDir = process.cwd()) { fs.writeSync(fd, content, 'utf8'); fs.fsyncSync(fd); fs.closeSync(fd); - fs.renameSync(tmpFilePath, revFilePath); const currentPointerPath = path.join(stateDir, 'current.json'); @@ -72,14 +106,13 @@ export function saveStateRevision(state, rootDir = process.cwd()) { currentRevision: state.stateRevision, currentRevisionFile: revFileName, workflowId: state.workflowId, - updatedAt: new Date().toISOString() + updatedAt: new Date().toISOString(), }, null, 2); const pFd = fs.openSync(tmpPointerPath, 'w'); fs.writeSync(pFd, pointerContent, 'utf8'); fs.fsyncSync(pFd); fs.closeSync(pFd); - fs.renameSync(tmpPointerPath, currentPointerPath); return state; } finally { @@ -87,12 +120,30 @@ export function saveStateRevision(state, rootDir = process.cwd()) { } } -export function recoverLatestValidState(rootDir = process.cwd()) { +export function saveStateRevision(state, rootDir = process.cwd()) { + validateWorkflowState(state); + + if (!shouldUseV2(rootDir)) { + return saveLegacyStateRevision(state, rootDir); + } + + appendEntityState({ + rootDir, + entityType: 'autopilot-workflow', + entityId: state.workflowId, + state, + actorClass: 'autopilot', + timestamp: state.updatedAt, + }); + return state; +} + +function recoverLatestLegacyState(rootDir) { const stateDir = getStateDir(rootDir); if (!fs.existsSync(stateDir)) return null; const files = fs.readdirSync(stateDir) - .filter(f => /^revision-\d{6}\.json$/.test(f)) + .filter((file) => /^revision-\d{6}\.json$/.test(file)) .sort() .reverse(); @@ -102,21 +153,33 @@ export function recoverLatestValidState(rootDir = process.cwd()) { const state = JSON.parse(fs.readFileSync(filePath, 'utf8')); validateWorkflowState(state); - // Repair pointer const pointerContent = JSON.stringify({ currentRevision: state.stateRevision, currentRevisionFile: file, workflowId: state.workflowId, updatedAt: new Date().toISOString(), - recoveredAt: new Date().toISOString() + recoveredAt: new Date().toISOString(), }, null, 2); fs.writeFileSync(path.join(stateDir, 'current.json'), pointerContent, 'utf8'); return state; } catch { - // Try previous revision + // Try previous revision. } } return null; } + +export function recoverLatestValidState(rootDir = process.cwd()) { + if (shouldUseV2(rootDir) && isStateEngineV2Active(rootDir)) { + const current = migratedWorkflowState(rootDir); + if (current) { + rebuildStateSnapshot(rootDir); + const rebuilt = loadLatestEntityState('autopilot-workflow', rootDir); + validateWorkflowState(rebuilt); + return rebuilt; + } + } + return recoverLatestLegacyState(rootDir); +} From 26ff7ea253ba99d94a17641f0b81a6fd56ccb7c6 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:29:57 +0200 Subject: [PATCH 015/177] feat(v0.13): route orchestration state through V2 without weakening gate profiles --- runtime/orchestration/orchestration-run.mjs | 101 +++++++++++++++++--- 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/runtime/orchestration/orchestration-run.mjs b/runtime/orchestration/orchestration-run.mjs index e7ce9d7f7..316db5a6a 100644 --- a/runtime/orchestration/orchestration-run.mjs +++ b/runtime/orchestration/orchestration-run.mjs @@ -5,6 +5,13 @@ import { validateDevelopmentContract } from './development-contract.mjs'; import { resolveContractGateProfiles } from './gate-profiles.mjs'; import { selectExecutionStrategy } from './host-capabilities.mjs'; import { getRunDirectory } from './evidence-store.mjs'; +import { + appendEntityState, + canonicalStateJson, + hasCompletedStateMigration, + isStateEngineV2Active, + loadEntityState, +} from './state-engine-v2.mjs'; const RUN_STATES = Object.freeze([ 'READY', @@ -56,6 +63,28 @@ function persistImmutable(filePath, value, label) { return { created: true, path: filePath }; } +function runEntityId(contractId, runId) { + return `${contractId}/${runId}`; +} + +function legacyRunStateExists(contractId, runId, rootDir) { + const runDir = getRunDirectory(rootDir, contractId, runId); + const revisionDir = path.join(runDir, 'state-revisions'); + if (fs.existsSync(path.join(runDir, 'current-state.json'))) return true; + if (!fs.existsSync(revisionDir)) return false; + return fs.readdirSync(revisionDir).some((name) => /^\d{8}\.json$/.test(name)); +} + +function v2RunState(contractId, runId, rootDir) { + if (!isStateEngineV2Active(rootDir)) return null; + return loadEntityState('orchestration-run', runEntityId(contractId, runId), rootDir); +} + +function shouldUseV2RunState(contractId, runId, rootDir) { + if (!legacyRunStateExists(contractId, runId, rootDir)) return true; + return hasCompletedStateMigration(rootDir) && v2RunState(contractId, runId, rootDir) !== null; +} + export function createOrchestrationRun({ contract, runId, @@ -154,19 +183,57 @@ export function persistRunManifest(run, rootDir = process.cwd()) { export function persistRunStateRevision(run, rootDir = process.cwd()) { validateOrchestrationRun(run); - const runDir = getRunDirectory(rootDir, run.contractId, run.runId); - const revisionName = `${String(run.stateRevision).padStart(8, '0')}.json`; - const revisionPath = path.join(runDir, 'state-revisions', revisionName); - const persisted = persistImmutable(revisionPath, run, 'orchestration run state revision'); - const pointer = { - schemaVersion: '1.0.0', - contractId: run.contractId, - runId: run.runId, - stateRevision: run.stateRevision, - revisionPath: `state-revisions/${revisionName}`, + + if (!shouldUseV2RunState(run.contractId, run.runId, rootDir)) { + const runDir = getRunDirectory(rootDir, run.contractId, run.runId); + const revisionName = `${String(run.stateRevision).padStart(8, '0')}.json`; + const revisionPath = path.join(runDir, 'state-revisions', revisionName); + const persisted = persistImmutable(revisionPath, run, 'orchestration run state revision'); + const pointer = { + schemaVersion: '1.0.0', + contractId: run.contractId, + runId: run.runId, + stateRevision: run.stateRevision, + revisionPath: `state-revisions/${revisionName}`, + }; + atomicWrite(path.join(runDir, 'current-state.json'), stableContent(pointer)); + return { ...persisted, pointerPath: path.join(runDir, 'current-state.json'), stateEngine: 'legacy' }; + } + + const existing = v2RunState(run.contractId, run.runId, rootDir); + if (existing) { + if (run.stateRevision < existing.stateRevision) { + throw new OrchestrationRunError('Refusing orchestration run stateRevision regression'); + } + if (run.stateRevision === existing.stateRevision) { + if (canonicalStateJson(run) === canonicalStateJson(existing)) { + return { created: false, stateEngine: 'v2', path: null, pointerPath: null }; + } + throw new OrchestrationRunError('Refusing to overwrite orchestration run state revision'); + } + } + + const result = appendEntityState({ + rootDir, + entityType: 'orchestration-run', + entityId: runEntityId(run.contractId, run.runId), + state: run, + refs: { + contractId: run.contractId, + taskId: run.taskId, + runId: run.runId, + }, + actorClass: 'orchestration', + timestamp: run.updatedAt, + }); + + return { + created: result.changed, + stateEngine: 'v2', + path: null, + pointerPath: null, + eventId: result.event?.eventId ?? null, }; - atomicWrite(path.join(runDir, 'current-state.json'), stableContent(pointer)); - return { ...persisted, pointerPath: path.join(runDir, 'current-state.json') }; } export function persistFinalRunState(run, rootDir = process.cwd()) { @@ -185,6 +252,16 @@ export function loadRunManifest(contractId, runId, rootDir = process.cwd()) { } export function loadCurrentRunState(contractId, runId, rootDir = process.cwd()) { + if (shouldUseV2RunState(contractId, runId, rootDir)) { + const v2 = v2RunState(contractId, runId, rootDir); + if (v2) { + validateOrchestrationRun(v2); + if (v2.contractId !== contractId || v2.runId !== runId) { + throw new OrchestrationRunError('State Engine V2 run identity mismatch'); + } + return v2; + } + } const runDir = getRunDirectory(rootDir, contractId, runId); const pointerPath = path.join(runDir, 'current-state.json'); if (!fs.existsSync(pointerPath)) return loadRunManifest(contractId, runId, rootDir); From 68cfe6e9a2178293ab4d0d4676b340d550fea4f9 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:30:41 +0200 Subject: [PATCH 016/177] fix(v0.13): recover unwitnessed torn canonical ledger tails safely --- runtime/orchestration/state-engine-v2.mjs | 93 ++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index b6fed6155..c9457ca1d 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -631,6 +631,92 @@ function parseLedger(content) { return events; } +function acknowledgedSnapshotWitness(paths) { + if (!fs.existsSync(paths.snapshot)) return null; + try { + const snapshot = JSON.parse(fs.readFileSync(paths.snapshot, 'utf8')); + validateStateSnapshot(snapshot); + return snapshot; + } catch { + // A corrupt disposable/materialized snapshot cannot authorize destructive + // canonical-ledger repair. It will be rebuilt after canonical recovery. + return null; + } +} + +function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { + const paths = ensureStateEngineLayout(rootDir); + if (fs.existsSync(paths.pending)) { + // withStateLock() recovers a valid pending commit before invoking this + // function. Seeing one here means recovery did not safely resolve it. + throw new StateEngineError('Pending commit remains unresolved during trailing-ledger recovery'); + } + + const content = fs.readFileSync(paths.events, 'utf8'); + if (!content || content.endsWith('\n')) return parseLedger(content); + + const boundary = content.lastIndexOf('\n'); + const prefixContent = boundary === -1 ? '' : content.slice(0, boundary + 1); + const tail = content.slice(boundary + 1); + const prefixEvents = parseLedger(prefixContent); + const previous = prefixEvents.at(-1) ?? null; + + // A fully valid final event that merely lost its record terminator is + // recoverable without discarding canonical information. + try { + const event = JSON.parse(tail); + validateStateEvent(event, previous); + const fd = fs.openSync(paths.events, 'r+'); + try { + const stat = fs.fstatSync(fd); + if (!stat.isFile()) throw new StateEngineError('Canonical ledger repair target is not a regular file'); + fs.writeSync(fd, Buffer.from('\n'), 0, 1, Buffer.byteLength(content, 'utf8')); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + return [...prefixEvents, event]; + } catch (error) { + if (error instanceof StateEngineError && !String(error.message).includes('JSON')) { + // A parseable event with an invalid sequence/hash is ambiguous + // corruption, not a torn byte suffix. Never truncate it automatically. + throw error; + } + } + + // The final suffix is not valid JSON. It may be repaired only when no + // already-acknowledged snapshot claims a later canonical event. This keeps + // a missing Windows pending-journal directory entry from making a torn + // unacknowledged append unrecoverable, while preventing rollback of + // previously materialized history. + const witness = acknowledgedSnapshotWitness(paths); + if (witness && witness.lastEventSequence > prefixEvents.length) { + throw new StateEngineError( + 'Refusing to truncate trailing canonical bytes witnessed by the materialized snapshot', + { + code: 'ACKNOWLEDGED_LEDGER_TAIL_CORRUPT', + snapshotSequence: witness.lastEventSequence, + recoverablePrefixSequence: prefixEvents.length, + }, + ); + } + if (witness && witness.lastEventSequence === prefixEvents.length) { + const prefixHash = prefixEvents.at(-1)?.eventHash ?? null; + if (witness.lastEventHash !== prefixHash) { + throw new StateEngineError('Materialized snapshot hash disagrees with recoverable canonical prefix'); + } + } + + const fd = fs.openSync(paths.events, 'r+'); + try { + fs.ftruncateSync(fd, Buffer.byteLength(prefixContent, 'utf8')); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + return prefixEvents; +} + export function loadCanonicalEvents(rootDir = process.cwd()) { const paths = statePaths(rootDir); if (!fs.existsSync(paths.events)) { @@ -642,11 +728,16 @@ export function loadCanonicalEvents(rootDir = process.cwd()) { if (fs.existsSync(paths.pending)) { return withStateLock(rootDir, () => parseLedger(fs.readFileSync(paths.events, 'utf8'))); } + const content = fs.readFileSync(paths.events, 'utf8'); if (fs.existsSync(paths.pending)) { return withStateLock(rootDir, () => parseLedger(fs.readFileSync(paths.events, 'utf8'))); } - return parseLedger(content); + if (!content || content.endsWith('\n')) return parseLedger(content); + + // Only a non-terminated final record is eligible for automatic tail + // recovery. Mid-ledger corruption and hash-chain failures remain fail-closed. + return withStateLock(rootDir, () => repairUnwitnessedTrailingLedgerSuffixLocked(rootDir)); } function normalizePathSegments(value) { From f9a947c050aa344123e5c9f4698ad196fe303638 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:32:05 +0200 Subject: [PATCH 017/177] fix(v0.13): make Windows durability helper explicit and storage-aware --- scripts/windows-durable-rename.ps1 | 234 ++++++++++++++++++++++++++--- 1 file changed, 217 insertions(+), 17 deletions(-) diff --git a/scripts/windows-durable-rename.ps1 b/scripts/windows-durable-rename.ps1 index 7fe8bc790..613471cae 100644 --- a/scripts/windows-durable-rename.ps1 +++ b/scripts/windows-durable-rename.ps1 @@ -1,30 +1,230 @@ -# Windows offline recovery helper. Uses the native directory-move API with -# MOVEFILE_WRITE_THROUGH; Node.js fs.renameSync exposes no equivalent flag. -# Accept only literal path arguments from the trusted DKF recovery runtime. +# DKF v0.13 Windows State Engine durability helper. +# +# Narrow audited boundary for: +# - local storage-profile detection, +# - same-volume write-through directory rename, +# - write-capable parent-directory FlushFileBuffers barrier. +# +# This helper does NOT claim universal physical power-loss durability. Runtime +# recovery remains witness-based and fail-closed. Arguments are literal process +# arguments; this script never evaluates command text or interpolates a shell. param( - [Parameter(Mandatory = $true)][string]$From, - [Parameter(Mandatory = $true)][string]$To + [Parameter(Mandatory = $true)] + [ValidateSet('Probe', 'Rename', 'FlushDirectory')] + [string]$Operation, + + [string]$Path, + [string]$From, + [string]$To ) $ErrorActionPreference = 'Stop' -if ([string]::IsNullOrWhiteSpace($From) -or [string]::IsNullOrWhiteSpace($To)) { - throw 'Both directory paths are required' -} -# The native API checks that the source exists and the destination does not. -# Avoid .NET file-path normalization of extended-length (\\?\) Win32 paths. $source = @' using System; +using System.ComponentModel; using System.Runtime.InteropServices; -public static class DkfWindowsDurableRename { - [DllImport("kernel32.dll", EntryPoint = "MoveFileExW", CharSet = CharSet.Unicode, SetLastError = true)] +using System.Text; +using Microsoft.Win32.SafeHandles; + +public static class DkfWindowsDurability { + public const uint MOVEFILE_WRITE_THROUGH = 0x00000008; + public const uint GENERIC_WRITE = 0x40000000; + public const uint FILE_SHARE_READ = 0x00000001; + public const uint FILE_SHARE_WRITE = 0x00000002; + public const uint FILE_SHARE_DELETE = 0x00000004; + public const uint OPEN_EXISTING = 3; + public const uint FILE_FLAG_WRITE_THROUGH = 0x80000000; + public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + public const uint DRIVE_FIXED = 3; + + [DllImport("kernel32.dll", EntryPoint = "MoveFileExW", + CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool MoveFileEx( + string existingName, + string newName, + uint flags); + + [DllImport("kernel32.dll", EntryPoint = "GetVolumePathNameW", + CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetVolumePathName( + string fileName, + StringBuilder volumePathName, + uint bufferLength); + + [DllImport("kernel32.dll", EntryPoint = "GetVolumeInformationW", + CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool MoveFileEx(string source, string destination, uint flags); + private static extern bool GetVolumeInformation( + string rootPathName, + StringBuilder volumeNameBuffer, + uint volumeNameSize, + out uint volumeSerialNumber, + out uint maximumComponentLength, + out uint fileSystemFlags, + StringBuilder fileSystemNameBuffer, + uint fileSystemNameSize); + + [DllImport("kernel32.dll", EntryPoint = "GetDriveTypeW", + CharSet = CharSet.Unicode)] + private static extern uint GetDriveType(string rootPathName); + + [DllImport("kernel32.dll", EntryPoint = "CreateFileW", + CharSet = CharSet.Unicode, SetLastError = true)] + private static extern SafeFileHandle CreateFile( + string fileName, + uint desiredAccess, + uint shareMode, + IntPtr securityAttributes, + uint creationDisposition, + uint flagsAndAttributes, + IntPtr templateFile); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool FlushFileBuffers(SafeFileHandle handle); + + private static Win32Exception LastError(string operation) { + return new Win32Exception(Marshal.GetLastWin32Error(), operation); + } + + public static string VolumeRoot(string path) { + var buffer = new StringBuilder(32768); + if (!GetVolumePathName(path, buffer, (uint)buffer.Capacity)) { + throw LastError("GetVolumePathNameW failed"); + } + return buffer.ToString(); + } + + public static string FileSystem(string root) { + var volume = new StringBuilder(261); + var fs = new StringBuilder(261); + uint serial; + uint maxComponent; + uint flags; + if (!GetVolumeInformation( + root, volume, (uint)volume.Capacity, + out serial, out maxComponent, out flags, + fs, (uint)fs.Capacity)) { + throw LastError("GetVolumeInformationW failed"); + } + return fs.ToString(); + } + + public static uint DriveType(string root) { + return GetDriveType(root); + } + + public static void FlushDirectory(string directory) { + const uint share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + const uint flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_WRITE_THROUGH; + using (var handle = CreateFile( + directory, + GENERIC_WRITE, + share, + IntPtr.Zero, + OPEN_EXISTING, + flags, + IntPtr.Zero)) { + if (handle.IsInvalid) { + throw LastError("CreateFileW directory durability handle failed"); + } + if (!FlushFileBuffers(handle)) { + throw LastError("FlushFileBuffers directory barrier failed"); + } + } + } + + public static void RenameWriteThrough(string from, string to) { + if (!MoveFileEx(from, to, MOVEFILE_WRITE_THROUGH)) { + throw LastError("MoveFileExW(MOVEFILE_WRITE_THROUGH) failed"); + } + } } '@ + Add-Type -TypeDefinition $source -ErrorAction Stop -$MOVEFILE_WRITE_THROUGH = [uint32]8 -if (-not [DkfWindowsDurableRename]::MoveFileEx($From, $To, $MOVEFILE_WRITE_THROUGH)) { - $win32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() - throw ([System.ComponentModel.Win32Exception]::new($win32Error)) + +function Get-DkfStorageProfile([string]$InputPath) { + if ([string]::IsNullOrWhiteSpace($InputPath)) { + throw 'Path is required' + } + $full = [System.IO.Path]::GetFullPath($InputPath) + $root = [DkfWindowsDurability]::VolumeRoot($full) + $fileSystem = [DkfWindowsDurability]::FileSystem($root) + $driveType = [DkfWindowsDurability]::DriveType($root) + [PSCustomObject]@{ + path = $full + volumeRoot = $root + fileSystem = $fileSystem + driveType = [int]$driveType + localFixed = ($driveType -eq [DkfWindowsDurability]::DRIVE_FIXED) + supportedAutomaticRestore = ( + $driveType -eq [DkfWindowsDurability]::DRIVE_FIXED -and + $fileSystem -eq 'NTFS' + ) + } +} + +switch ($Operation) { + 'Probe' { + $profile = Get-DkfStorageProfile $Path + $profile | ConvertTo-Json -Compress + break + } + + 'FlushDirectory' { + $profile = Get-DkfStorageProfile $Path + if (-not $profile.supportedAutomaticRestore) { + throw ("Unsupported automatic Windows durability profile: filesystem={0}, driveType={1}" -f + $profile.fileSystem, $profile.driveType) + } + [DkfWindowsDurability]::FlushDirectory($profile.path) + [PSCustomObject]@{ + operation = 'FlushDirectory' + profile = $profile + flushed = $true + } | ConvertTo-Json -Compress -Depth 4 + break + } + + 'Rename' { + if ([string]::IsNullOrWhiteSpace($From) -or [string]::IsNullOrWhiteSpace($To)) { + throw 'From and To are required for Rename' + } + $sourcePath = [System.IO.Path]::GetFullPath($From) + $destinationPath = [System.IO.Path]::GetFullPath($To) + $destinationParent = [System.IO.Path]::GetDirectoryName($destinationPath) + $sourceProfile = Get-DkfStorageProfile $sourcePath + $destinationProfile = Get-DkfStorageProfile $destinationParent + + if (-not $sourceProfile.supportedAutomaticRestore -or + -not $destinationProfile.supportedAutomaticRestore) { + throw ("Unsupported automatic Windows durability profile: source={0}/{1}, destination={2}/{3}" -f + $sourceProfile.fileSystem, $sourceProfile.driveType, + $destinationProfile.fileSystem, $destinationProfile.driveType) + } + if ($sourceProfile.volumeRoot -ne $destinationProfile.volumeRoot) { + throw 'Durable directory rename requires source and destination on the same volume' + } + if ([System.IO.Directory]::Exists($destinationPath) -or + [System.IO.File]::Exists($destinationPath)) { + throw 'Durable rename destination already exists' + } + + [DkfWindowsDurability]::RenameWriteThrough($sourcePath, $destinationPath) + [DkfWindowsDurability]::FlushDirectory($destinationParent) + + [PSCustomObject]@{ + operation = 'Rename' + source = $sourcePath + destination = $destinationPath + profile = $destinationProfile + renamed = $true + parentFlushCompleted = $true + } | ConvertTo-Json -Compress -Depth 4 + break + } } From 4ed9b103c0a6170cb5f26e653aad06c1059406de Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:32:51 +0200 Subject: [PATCH 018/177] fix(v0.13): wire storage-aware Windows durability helper into restore runtime --- .../orchestration/state-engine-migration.mjs | 94 ++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index 7ffcc4ada..75a0fe413 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -1,6 +1,8 @@ import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; import { validateWorkflowState } from '../autopilot/validators.mjs'; import { validateOrchestrationRun } from './orchestration-run.mjs'; @@ -598,9 +600,87 @@ function restoreJournalPath(targetRoot) { * Any failure is fatal: callers never mutate or acknowledge past a barrier * that did not complete, so the journal and witnesses are retained. */ +const WINDOWS_DURABILITY_HELPER = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../scripts/windows-durable-rename.ps1', +); + +function runWindowsDurabilityHelper(operation, args = {}) { + if (process.platform !== 'win32') { + throw new StateMigrationError('Windows durability helper may only run on Windows'); + } + if (!fs.existsSync(WINDOWS_DURABILITY_HELPER)) { + throw new StateMigrationError('Windows durability helper is missing', { + code: 'WINDOWS_DURABILITY_HELPER_MISSING', + helper: WINDOWS_DURABILITY_HELPER, + }); + } + + const argv = [ + '-NoLogo', + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', 'Bypass', + '-File', WINDOWS_DURABILITY_HELPER, + '-Operation', operation, + ]; + for (const [key, value] of Object.entries(args)) { + if (value === null || value === undefined) continue; + argv.push('-' + key, String(value)); + } + + const result = spawnSync('powershell.exe', argv, { + encoding: 'utf8', + windowsHide: true, + shell: false, + timeout: 30_000, + }); + if (result.error || result.status !== 0) { + throw new StateMigrationError('Windows durability helper failed; recovery evidence retained', { + code: 'WINDOWS_DURABILITY_HELPER_FAILED', + operation, + status: result.status, + cause: result.error?.message ?? String(result.stderr || result.stdout || 'unknown failure').trim(), + }); + } + + const output = String(result.stdout || '').trim(); + if (!output) { + throw new StateMigrationError('Windows durability helper returned no result'); + } + try { + return JSON.parse(output.split(/\r?\n/).filter(Boolean).at(-1)); + } catch (error) { + throw new StateMigrationError('Windows durability helper returned invalid JSON', { + operation, + cause: error.message, + }); + } +} + +function requireSupportedWindowsRestoreProfile(targetPath) { + if (process.platform !== 'win32') return null; + const profile = runWindowsDurabilityHelper('Probe', { Path: targetPath }); + if (profile?.supportedAutomaticRestore !== true + || String(profile.fileSystem || '').toUpperCase() !== 'NTFS' + || profile.localFixed !== true) { + throw new StateMigrationError( + 'Windows automatic restore requires a local fixed NTFS durability profile', + { + code: 'UNSUPPORTED_WINDOWS_DURABILITY_TARGET', + profile, + }, + ); + } + return profile; +} + function syncRestoreParent(parent) { let fd; try { + // This syscall barrier is useful on both platforms but is not treated as + // the sole recovery witness on Windows. The transaction journal/stage + // witnesses remain authoritative across restart ambiguity. fd = fs.openSync(parent, process.platform === 'win32' ? 'r+' : 'r'); fs.fsyncSync(fd); } catch (error) { @@ -612,9 +692,19 @@ function syncRestoreParent(parent) { } } -function durableRestoreRollbackRename(from, to) { +function durableRestoreRename(from, to) { + const parent = path.dirname(to); + if (process.platform === 'win32') { + requireSupportedWindowsRestoreProfile(parent); + runWindowsDurabilityHelper('Rename', { From: from, To: to }); + return; + } fs.renameSync(from, to); - syncRestoreParent(path.dirname(to)); + syncRestoreParent(parent); +} + +function durableRestoreRollbackRename(from, to) { + durableRestoreRename(from, to); } /** From ce934b4e6467e33a8c30dfd250fae0c88f8fd8c4 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:33:55 +0200 Subject: [PATCH 019/177] feat(v0.13): add persistent hash-chained restore transaction log --- .../orchestration/state-engine-migration.mjs | 191 +++++++++++++++++- 1 file changed, 188 insertions(+), 3 deletions(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index 75a0fe413..fa45f7c50 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -577,11 +577,196 @@ function resolveSafeRestoreDestination(targetRoot, relativePath, { createMissing } -/** A deterministic sibling journal lets offline recovery find interrupted root promotion. */ +export const RESTORE_TRANSACTION_SCHEMA_VERSION = '2.0.0'; +const RESTORE_TERMINAL_PHASE = 'CLEANUP_SAFE'; +const RESTORE_PHASES = Object.freeze([ + 'PREPARED', + 'ORIGINAL_DISPLACED', + 'CANDIDATE_PUBLISHED', + 'PROMOTION_ACKNOWLEDGED', + RESTORE_TERMINAL_PHASE, +]); + +function restoreJournalDigest(targetRoot) { + return crypto.createHash('sha256').update(path.resolve(targetRoot)).digest('hex').slice(0, 24); +} + +/** + * Stable sibling transaction log. It lives outside the root being replaced and + * is intentionally retained after successful restore. New transactions append + * to the same hash-chained JSONL history. + */ function restoreJournalPath(targetRoot) { const root = path.resolve(targetRoot); - const digest = crypto.createHash('sha256').update(root).digest('hex').slice(0, 24); - return path.join(path.dirname(root), '.dk-restore-journal-' + digest + '.json'); + return path.join(path.dirname(root), '.dk-restore-transaction-' + restoreJournalDigest(root) + '.jsonl'); +} + +/** Pre-v0.13 single-record journal retained for explicit compatibility recovery. */ +function legacyRestoreJournalPath(targetRoot) { + const root = path.resolve(targetRoot); + return path.join(path.dirname(root), '.dk-restore-journal-' + restoreJournalDigest(root) + '.json'); +} + +function restoreRecordHash(record) { + const { recordHash, ...payload } = record; + return stateSha256(Buffer.from(JSON.stringify(stable(payload)))); +} + +function validateRestoreRecord(record, previous = null) { + if (!plainObject(record) || record.schemaVersion !== RESTORE_TRANSACTION_SCHEMA_VERSION) { + throw new StateMigrationError('Restore transaction record schema is invalid'); + } + if (!Number.isInteger(record.sequence) || record.sequence < 1) { + throw new StateMigrationError('Restore transaction sequence is invalid'); + } + if (!/^[a-f0-9]{32}$/.test(record.transactionId || '')) { + throw new StateMigrationError('Restore transactionId is invalid'); + } + if (!RESTORE_PHASES.includes(record.phase)) { + throw new StateMigrationError('Restore transaction phase is invalid'); + } + if (typeof record.timestamp !== 'string' || Number.isNaN(Date.parse(record.timestamp))) { + throw new StateMigrationError('Restore transaction timestamp is invalid'); + } + if (typeof record.targetRoot !== 'string' || path.resolve(record.targetRoot) !== record.targetRoot) { + throw new StateMigrationError('Restore transaction targetRoot is invalid'); + } + if (typeof record.stageName !== 'string' || !/^\.dk-legacy-restore-stage-[a-f0-9]{32}$/.test(record.stageName)) { + throw new StateMigrationError('Restore transaction stageName is invalid'); + } + if (record.existed !== true && record.existed !== false) { + throw new StateMigrationError('Restore transaction existed witness is invalid'); + } + if (!/^\d+$/.test(record.stageDev || '') || !/^\d+$/.test(record.stageIno || '')) { + throw new StateMigrationError('Restore transaction stage identity is invalid'); + } + if (record.existed && (!/^\d+$/.test(record.originalDev || '') || !/^\d+$/.test(record.originalIno || ''))) { + throw new StateMigrationError('Restore transaction original identity is invalid'); + } + if (previous === null) { + if (record.sequence !== 1 || record.previousRecordHash !== null) { + throw new StateMigrationError('First restore record chain witness is invalid'); + } + } else { + if (record.sequence !== previous.sequence + 1) { + throw new StateMigrationError('Restore transaction sequence is not contiguous'); + } + if (record.previousRecordHash !== previous.recordHash) { + throw new StateMigrationError('Restore transaction previous hash does not match'); + } + } + if (!/^sha256:[a-f0-9]{64}$/.test(record.recordHash || '') + || record.recordHash !== restoreRecordHash(record)) { + throw new StateMigrationError('Restore transaction record hash mismatch'); + } + return true; +} + +function loadRestoreJournal(targetRoot) { + const journal = restoreJournalPath(targetRoot); + if (!fs.existsSync(journal)) return []; + const content = fs.readFileSync(journal, 'utf8'); + if (!content) return []; + if (!content.endsWith('\n')) { + throw new StateMigrationError('Restore transaction log has an incomplete trailing record; preserve it for operator recovery'); + } + const lines = content.split('\n').filter(Boolean); + const records = []; + let previous = null; + for (const line of lines) { + let record; + try { + record = JSON.parse(line); + } catch (error) { + throw new StateMigrationError('Restore transaction log contains invalid JSON: ' + error.message); + } + validateRestoreRecord(record, previous); + records.push(record); + previous = record; + } + return records; +} + +function latestRestoreTransaction(records) { + if (!Array.isArray(records) || records.length === 0) return null; + const transactionId = records.at(-1).transactionId; + const txRecords = records.filter((record) => record.transactionId === transactionId); + const prepared = txRecords.find((record) => record.phase === 'PREPARED'); + if (!prepared) throw new StateMigrationError('Latest restore transaction is missing PREPARED witness'); + return { + prepared, + last: txRecords.at(-1), + records: txRecords, + }; +} + +function appendRestoreTransactionRecord({ + targetRoot, + transaction, + phase, + outcome = null, + now = new Date().toISOString(), +}) { + if (!RESTORE_PHASES.includes(phase)) throw new StateMigrationError('Unknown restore transaction phase: ' + phase); + const journal = restoreJournalPath(targetRoot); + const records = loadRestoreJournal(targetRoot); + const previous = records.at(-1) ?? null; + const base = { + schemaVersion: RESTORE_TRANSACTION_SCHEMA_VERSION, + sequence: (previous?.sequence ?? 0) + 1, + transactionId: transaction.transactionId, + phase, + timestamp: now, + targetRoot: transaction.targetRoot, + stageName: transaction.stageName, + existed: transaction.existed, + originalDev: transaction.originalDev, + originalIno: transaction.originalIno, + stageDev: transaction.stageDev, + stageIno: transaction.stageIno, + outcome, + previousRecordHash: previous?.recordHash ?? null, + }; + const record = { ...base, recordHash: restoreRecordHash(base) }; + + fs.mkdirSync(path.dirname(journal), { recursive: true }); + const existed = fs.existsSync(journal); + const fd = fs.openSync(journal, 'a', 0o600); + try { + fs.writeFileSync(fd, JSON.stringify(record) + '\n', 'utf8'); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + // On first publication this is a namespace durability barrier. On Windows + // it is not the sole witness: orphan stage/displaced directories are also + // detected before any future destructive transaction. + if (!existed) syncRestoreParent(path.dirname(journal)); + return Object.freeze(record); +} + +function orphanRestoreWitnesses(targetRoot) { + const root = path.resolve(targetRoot); + const parent = path.dirname(root); + if (!fs.existsSync(parent)) return []; + return fs.readdirSync(parent) + .filter((name) => /^\.dk-legacy-restore-stage-[a-f0-9]{32}(?:-original)?$/.test(name)) + .map((name) => path.join(parent, name)) + .sort(); +} + +function assertNoUnjournaledRestoreWitness(targetRoot) { + if (fs.existsSync(restoreJournalPath(targetRoot)) || fs.existsSync(legacyRestoreJournalPath(targetRoot))) return; + const witnesses = orphanRestoreWitnesses(targetRoot); + if (witnesses.length > 0) { + throw new StateMigrationError( + 'Restore witnesses exist without a recovery journal; refusing destructive mutation', + { + code: 'RESTORE_WITNESS_WITHOUT_JOURNAL', + witnesses, + }, + ); + } } /** From 8915902285ee01f272b27ca41d432b9183cb015d Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:36:30 +0200 Subject: [PATCH 020/177] fix(v0.13): make restore recovery persistent and witness-driven --- .../orchestration/state-engine-migration.mjs | 307 +++++++++++++++--- 1 file changed, 254 insertions(+), 53 deletions(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index fa45f7c50..cc5d52bfe 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -915,18 +915,146 @@ function syncRestoreTree(directory) { syncRestoreParent(directory); } -function createRestoreJournal(file, data) { - const fd = fs.openSync(file, 'wx', 0o600); +function inspectRestorePath(file) { try { - fs.writeFileSync(fd, stablePretty(data)); - fs.fsyncSync(fd); - } finally { fs.closeSync(fd); } - syncRestoreParent(path.dirname(file)); + return fs.lstatSync(file, { bigint: true }); + } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } +} + +function sameRestoreIdentity(stat, dev, ino) { + return Boolean(stat) + && stat.isDirectory() + && !stat.isSymbolicLink() + && stat.dev.toString() === dev + && stat.ino.toString() === ino; +} + +function appendMissingSuccessfulRestorePhases(targetRoot, transaction, existingRecords, { outcome = 'promoted' } = {}) { + const phases = new Set(existingRecords.map((record) => record.phase)); + if (transaction.existed && !phases.has('ORIGINAL_DISPLACED')) { + appendRestoreTransactionRecord({ targetRoot, transaction, phase: 'ORIGINAL_DISPLACED' }); + phases.add('ORIGINAL_DISPLACED'); + } + if (!phases.has('CANDIDATE_PUBLISHED')) { + appendRestoreTransactionRecord({ targetRoot, transaction, phase: 'CANDIDATE_PUBLISHED' }); + phases.add('CANDIDATE_PUBLISHED'); + } + if (!phases.has('PROMOTION_ACKNOWLEDGED')) { + appendRestoreTransactionRecord({ targetRoot, transaction, phase: 'PROMOTION_ACKNOWLEDGED' }); + phases.add('PROMOTION_ACKNOWLEDGED'); + } + if (!phases.has(RESTORE_TERMINAL_PHASE)) { + appendRestoreTransactionRecord({ + targetRoot, + transaction, + phase: RESTORE_TERMINAL_PHASE, + outcome, + }); + } +} + +function retainRecoveredLegacyRestoreJournal(journalPath, transactionId) { + const retained = journalPath + '.retained-' + transactionId + '.json'; + if (fs.existsSync(retained)) { + throw new StateMigrationError('Retained legacy restore journal already exists; refusing ambiguous cleanup', { + code: 'LEGACY_RESTORE_RETAINED_JOURNAL_CONFLICT', + retained, + }); + } + durableRestoreRename(journalPath, retained); + return retained; +} + +function recoverLegacyRestoreV1({ root, parent, journalPath }) { + const tx = readJson(journalPath, 'Legacy restore journal'); + const expectedStage = '.dk-legacy-restore-stage-' + tx.transactionId; + const stage = path.join(parent, expectedStage); + const displaced = stage + '-original'; + if (!/^[a-f0-9]{32}$/.test(tx.transactionId || '') + || tx.targetRoot !== root + || tx.schemaVersion !== '1.0.0' + || tx.stageName !== expectedStage + || (tx.existed !== true && tx.existed !== false) + || !/^\d+$/.test(tx.stageDev || '') + || !/^\d+$/.test(tx.stageIno || '') + || (tx.existed && (!/^\d+$/.test(tx.originalDev || '') || !/^\d+$/.test(tx.originalIno || '')))) { + throw new StateMigrationError('Legacy restore journal contains an invalid transaction witness'); + } + + if (process.platform === 'win32') requireSupportedWindowsRestoreProfile(parent); + + const atRoot = inspectRestorePath(root); + const inStage = inspectRestorePath(stage); + const inDisplaced = inspectRestorePath(displaced); + if (inStage && !sameRestoreIdentity(inStage, tx.stageDev, tx.stageIno)) { + throw new StateMigrationError('Unrecognized staged restore directory; refusing v1 recovery'); + } + if (inDisplaced && (!tx.existed || !sameRestoreIdentity(inDisplaced, tx.originalDev, tx.originalIno))) { + throw new StateMigrationError('Displaced restore directory differs from v1 original witness'); + } + + let outcome; + if (inDisplaced && !atRoot) { + durableRestoreRollbackRename(displaced, root); + outcome = 'rolled-back-original'; + } else if (inDisplaced && sameRestoreIdentity(atRoot, tx.stageDev, tx.stageIno)) { + outcome = 'promoted-original-retained'; + } else if (!inDisplaced && ( + (!atRoot && !tx.existed) + || (tx.existed && sameRestoreIdentity(atRoot, tx.originalDev, tx.originalIno)) + )) { + outcome = 'uncommitted-original-unchanged'; + } else if (!tx.existed && sameRestoreIdentity(atRoot, tx.stageDev, tx.stageIno)) { + outcome = 'promoted-new-root'; + } else { + throw new StateMigrationError('Ambiguous v1 restore journal state; refusing automatic mutation'); + } + + if (inStage && fs.existsSync(stage)) { + fs.rmSync(stage, { recursive: true, force: true }); + syncRestoreParent(parent); + } + + const transaction = { + transactionId: tx.transactionId, + targetRoot: root, + stageName: expectedStage, + existed: tx.existed, + originalDev: tx.originalDev ?? null, + originalIno: tx.originalIno ?? null, + stageDev: tx.stageDev, + stageIno: tx.stageIno, + }; + appendRestoreTransactionRecord({ + targetRoot: root, + transaction, + phase: 'PREPARED', + outcome: 'imported-v1-recovery', + }); + appendRestoreTransactionRecord({ + targetRoot: root, + transaction, + phase: RESTORE_TERMINAL_PHASE, + outcome: 'legacy-v1-' + outcome, + }); + + const retainedJournal = retainRecoveredLegacyRestoreJournal(journalPath, tx.transactionId); + return Object.freeze({ + recovered: true, + legacyJournal: true, + outcome, + retainedJournal, + displacedOriginal: outcome === 'promoted-original-retained' ? displaced : null, + }); } /** * Recover only an interrupted OFFLINE restore for the exact target root. - * Reject unexpected names, inode witnesses, or concurrent target mutations. + * Recovery is witness-driven and never treats absence of a journal entry as + * proof that no transaction existed. */ export function recoverLegacyRestore({ targetRoot, confirmOffline = false } = {}) { if (typeof targetRoot !== 'string' || !targetRoot) { @@ -935,67 +1063,140 @@ export function recoverLegacyRestore({ targetRoot, confirmOffline = false } = {} if (confirmOffline !== true) { throw new StateMigrationError('Explicit offline confirmation is required for restore recovery'); } + const root = path.resolve(targetRoot); const parent = path.dirname(root); - const journalPath = restoreJournalPath(root); - if (!fs.existsSync(journalPath)) return Object.freeze({ recovered: false }); - const tx = readJson(journalPath, 'Legacy restore journal'); + const legacyJournal = legacyRestoreJournalPath(root); + if (fs.existsSync(legacyJournal)) { + return recoverLegacyRestoreV1({ root, parent, journalPath: legacyJournal }); + } + + const records = loadRestoreJournal(root); + if (records.length === 0) { + assertNoUnjournaledRestoreWitness(root); + return Object.freeze({ recovered: false }); + } + + const latest = latestRestoreTransaction(records); + const tx = latest.prepared; const expectedStage = '.dk-legacy-restore-stage-' + tx.transactionId; + if (tx.targetRoot !== root || tx.stageName !== expectedStage) { + throw new StateMigrationError('Restore transaction witness does not match requested target root'); + } + const stage = path.join(parent, expectedStage); const displaced = stage + '-original'; - if (!/^[a-f0-9]{32}$/.test(tx.transactionId || '') || - tx.targetRoot !== root || tx.schemaVersion !== '1.0.0' || - tx.stageName !== expectedStage || tx.existed !== true && tx.existed !== false || - !/^\d+$/.test(tx.stageDev || '') || !/^\d+$/.test(tx.stageIno || '') || - (tx.existed && (!/^\d+$/.test(tx.originalDev || '') || !/^\d+$/.test(tx.originalIno || '')))) { - throw new StateMigrationError('Legacy restore journal contains an invalid transaction witness'); + + if (latest.last.phase === RESTORE_TERMINAL_PHASE) { + return Object.freeze({ + recovered: false, + alreadyComplete: true, + transactionId: tx.transactionId, + outcome: latest.last.outcome, + transactionLog: restoreJournalPath(root), + }); } - // The recovery runs in an explicitly quiescent trusted workspace. All - // location/inode witnesses are rechecked before any cleanup. - const inspect = file => { - try { return fs.lstatSync(file, { bigint: true }); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } - }; - const atRoot = inspect(root); - const inStage = inspect(stage); - const inDisplaced = inspect(displaced); - const equal = (stat, dev, ino) => stat && stat.isDirectory() && !stat.isSymbolicLink() && - stat.dev.toString() === dev && stat.ino.toString() === ino; - if (inStage && !equal(inStage, tx.stageDev, tx.stageIno)) { + + if (process.platform === 'win32') requireSupportedWindowsRestoreProfile(parent); + + const atRoot = inspectRestorePath(root); + const inStage = inspectRestorePath(stage); + const inDisplaced = inspectRestorePath(displaced); + + if (inStage && !sameRestoreIdentity(inStage, tx.stageDev, tx.stageIno)) { throw new StateMigrationError('Unrecognized staged restore directory; refusing recovery'); } - if (inDisplaced && (!tx.existed || !equal(inDisplaced, tx.originalDev, tx.originalIno))) { + if (inDisplaced && (!tx.existed || !sameRestoreIdentity(inDisplaced, tx.originalDev, tx.originalIno))) { throw new StateMigrationError('Displaced restore directory differs from original witness'); } - let outcome; - // Pre-mutation capability probe (fails closed with every witness intact). - syncRestoreParent(parent); + // Crash after original displacement but before candidate publication: restore + // the authenticated original and terminate the transaction as rolled back. if (inDisplaced && !atRoot) { - // The restored root directory entry must be durable before removing the - // only journal that permits redoing this rollback after another power loss. - durableRestoreRollbackRename(displaced, root); // crash after displacement - outcome = 'rolled-back-original'; - } else if (inDisplaced && equal(atRoot, tx.stageDev, tx.stageIno)) { - // Promotion completed; preserve original in displaced directory for - // explicit/manual audit rather than silently deleting recovery evidence. - outcome = 'promoted-original-retained'; - } else if (!inDisplaced && (!atRoot && !tx.existed || - (tx.existed && equal(atRoot, tx.originalDev, tx.originalIno)))) { - outcome = 'uncommitted-original-unchanged'; - } else if (!tx.existed && equal(atRoot, tx.stageDev, tx.stageIno)) { - outcome = 'promoted-new-root'; - } else { - throw new StateMigrationError('Ambiguous restore journal state; refusing automatic mutation'); + durableRestoreRollbackRename(displaced, root); + if (inStage && fs.existsSync(stage)) { + fs.rmSync(stage, { recursive: true, force: true }); + syncRestoreParent(parent); + } + appendRestoreTransactionRecord({ + targetRoot: root, + transaction: tx, + phase: RESTORE_TERMINAL_PHASE, + outcome: 'rolled-back-original', + }); + return Object.freeze({ + recovered: true, + outcome: 'rolled-back-original', + transactionId: tx.transactionId, + transactionLog: restoreJournalPath(root), + }); } - if (inStage) { - fs.rmSync(stage, { recursive: true, force: true }); - syncRestoreParent(parent); + // Candidate is already visible. Finish only the missing durable state + // records, then make old-root cleanup a post-terminal maintenance action. + if (sameRestoreIdentity(atRoot, tx.stageDev, tx.stageIno) + && ((!tx.existed && !inDisplaced) + || (tx.existed && inDisplaced))) { + appendMissingSuccessfulRestorePhases(root, tx, latest.records, { + outcome: tx.existed ? 'promoted-original-retained' : 'promoted-new-root', + }); + + let cleanupPending = false; + if (tx.existed && fs.existsSync(displaced)) { + try { + fs.rmSync(displaced, { recursive: true, force: true }); + syncRestoreParent(parent); + } catch { + cleanupPending = true; + } + } + if (inStage && fs.existsSync(stage)) { + try { + fs.rmSync(stage, { recursive: true, force: true }); + syncRestoreParent(parent); + } catch { + cleanupPending = true; + } + } + + return Object.freeze({ + recovered: true, + outcome: tx.existed ? 'promoted-original-retained' : 'promoted-new-root', + transactionId: tx.transactionId, + transactionLog: restoreJournalPath(root), + cleanupPending, + displacedOriginal: cleanupPending && tx.existed ? displaced : null, + }); } - // Keep any promoted displaced original as a recovery asset: never auto-delete. - fs.unlinkSync(journalPath); - syncRestoreParent(parent); - return Object.freeze({ recovered: true, outcome, displacedOriginal: outcome === 'promoted-original-retained' ? displaced : null }); + + // No destructive rename persisted. Preserve the authenticated original, drop + // only the authenticated staging copy, and terminally record rollback. + if (!inDisplaced && ( + (tx.existed && sameRestoreIdentity(atRoot, tx.originalDev, tx.originalIno)) + || (!tx.existed && !atRoot) + )) { + if (inStage && fs.existsSync(stage)) { + fs.rmSync(stage, { recursive: true, force: true }); + syncRestoreParent(parent); + } + appendRestoreTransactionRecord({ + targetRoot: root, + transaction: tx, + phase: RESTORE_TERMINAL_PHASE, + outcome: 'uncommitted-original-unchanged', + }); + return Object.freeze({ + recovered: true, + outcome: 'uncommitted-original-unchanged', + transactionId: tx.transactionId, + transactionLog: restoreJournalPath(root), + }); + } + + throw new StateMigrationError('Ambiguous restore transaction state; refusing automatic mutation', { + code: 'AMBIGUOUS_RESTORE_TRANSACTION', + transactionId: tx.transactionId, + }); } /** From ed9dbf24a87b6531c699d5a885ed2757f7ad3a6e Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:37:19 +0200 Subject: [PATCH 021/177] fix(v0.13): make restore promotion transactionally persistent --- .../orchestration/state-engine-migration.mjs | 173 ++++++++++++------ 1 file changed, 119 insertions(+), 54 deletions(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index cc5d52bfe..ba56fff2b 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -861,12 +861,15 @@ function requireSupportedWindowsRestoreProfile(targetPath) { } function syncRestoreParent(parent) { + if (process.platform === 'win32') { + requireSupportedWindowsRestoreProfile(parent); + runWindowsDurabilityHelper('FlushDirectory', { Path: parent }); + return; + } + let fd; try { - // This syscall barrier is useful on both platforms but is not treated as - // the sole recovery witness on Windows. The transaction journal/stage - // witnesses remain authoritative across restart ambiguity. - fd = fs.openSync(parent, process.platform === 'win32' ? 'r+' : 'r'); + fd = fs.openSync(parent, 'r'); fs.fsyncSync(fd); } catch (error) { throw new StateMigrationError('Restore directory flush barrier failed; recovery evidence retained', { @@ -1257,9 +1260,28 @@ export function restoreLegacyBackup({ if (confirmOffline !== true) { throw new StateMigrationError('Legacy backup restoration requires explicit offline confirmation and an exclusively controlled local workspace'); } - if (fs.existsSync(restoreJournalPath(targetRoot))) { - throw new StateMigrationError('Incomplete legacy restore exists; run recoverLegacyRestore with offline confirmation first'); + + const resolvedRoot = path.resolve(targetRoot); + const parent = path.dirname(resolvedRoot); + assertNoUnjournaledRestoreWitness(resolvedRoot); + + const legacyJournal = legacyRestoreJournalPath(resolvedRoot); + if (fs.existsSync(legacyJournal)) { + throw new StateMigrationError('Incomplete pre-v0.13 legacy restore exists; run recoverLegacyRestore with offline confirmation first'); + } + + const priorRecords = loadRestoreJournal(resolvedRoot); + if (priorRecords.length > 0) { + const latest = latestRestoreTransaction(priorRecords); + if (latest.last.phase !== RESTORE_TERMINAL_PHASE) { + throw new StateMigrationError('Incomplete legacy restore exists; run recoverLegacyRestore with offline confirmation first', { + code: 'INCOMPLETE_RESTORE_TRANSACTION', + transactionId: latest.prepared.transactionId, + phase: latest.last.phase, + }); + } } + const bundle = readJson(backupPath, 'Legacy backup'); validateBackupBundle(bundle); @@ -1267,14 +1289,10 @@ export function restoreLegacyBackup({ // even one byte; a later malicious backup path cannot leave a partial restore. const destinations = bundle.files.map((entry) => ({ entry, - destination: resolveSafeRestoreDestination(targetRoot, entry.path), + destination: resolveSafeRestoreDestination(resolvedRoot, entry.path), })); const normalizedDestinations = new Set(); for (const { entry, destination } of destinations) { - // Conservative case/Unicode folding and Win32 trailing-dot/space removal - // prevents duplicate-target aliases on APFS/HFS+, NTFS, and FAT. - // Refusing distinct-case or aliased names on a case-sensitive mount is intentional: - // restore cannot safely assume a particular target filesystem policy. const key = destination .normalize('NFC') .toLowerCase() @@ -1292,13 +1310,6 @@ export function restoreLegacyBackup({ } } - // Path-based preflight + O_NOFOLLOW cannot secure an overwrite against a - // concurrent parent-directory swap. Build and fsync a complete replacement - // tree in an owner-only sibling directory. Publish by renaming the root - // directory itself; no restored byte is ever opened through an attacker- - // mutable path *inside* targetRoot. - const resolvedRoot = path.resolve(targetRoot); - const parent = path.dirname(resolvedRoot); const originalRootExists = fs.existsSync(resolvedRoot); const rootStat = originalRootExists ? fs.lstatSync(resolvedRoot) : null; const originalMetadata = originalRootExists ? collectRestoreMetadata(resolvedRoot) : []; @@ -1306,20 +1317,32 @@ export function restoreLegacyBackup({ if (rootStat && (!rootStat.isDirectory() || rootStat.isSymbolicLink())) { throw new StateMigrationError('Legacy backup restore root must be an ordinary directory'); } - const transactionId = crypto.randomBytes(16).toString('hex'); - // Pre-mutation capability probe: if this volume cannot complete the - // directory flush barrier, refuse before creating the stage or journal. + + // Windows support is positive/capability based. Unsupported storage fails + // before the staging tree or any destructive target-root mutation exists. + if (process.platform === 'win32') requireSupportedWindowsRestoreProfile(parent); syncRestoreParent(parent); + + const transactionId = crypto.randomBytes(16).toString('hex'); const stage = path.join(parent, '.dk-legacy-restore-stage-' + transactionId); - fs.mkdirSync(stage, { mode: 0o700 }); const displaced = stage + '-original'; + fs.mkdirSync(stage, { mode: 0o700 }); + let promoted = false; let displacedOriginal = false; + let transaction = null; + try { fs.chmodSync(stage, 0o700); if (originalRootExists) { - fs.cpSync(resolvedRoot, stage, { recursive: true, force: true, dereference: false, verbatimSymlinks: true }); + fs.cpSync(resolvedRoot, stage, { + recursive: true, + force: true, + dereference: false, + verbatimSymlinks: true, + }); } + const restored = []; for (const { entry } of destinations) { const destination = resolveSafeRestoreDestination(stage, entry.path, { createMissing: true }); @@ -1332,8 +1355,6 @@ export function restoreLegacyBackup({ } finally { fs.closeSync(fd); } - // An atomic rename replaces the *directory entry* even if the prior - // staging entry is a link; it never truncates an external hardlink inode. fs.renameSync(tempFile, destination); if (stateSha256(fs.readFileSync(destination)) !== entry.fingerprint) { throw new StateMigrationError('Staged legacy file failed fingerprint verification: ' + entry.path); @@ -1341,19 +1362,11 @@ export function restoreLegacyBackup({ restored.push(entry.path); } - // Seal and flush staged file contents before metadata restoration. - // Metadata is replayed post-order so source file owners and descendant - // traversal modes do not drift and historical parent atimes are applied - // after every directory traversal has finished. syncRestoreTree(stage); if (rootStat) applyRestoreMetadata(stage, originalMetadata); - // Durable journal precedes *both* root-directory renames. A crash after - // either rename is recoverable by an explicit offline recovery invocation. const stageStat = fs.lstatSync(stage, { bigint: true }); - const journal = restoreJournalPath(resolvedRoot); - createRestoreJournal(journal, { - schemaVersion: '1.0.0', + transaction = { transactionId, stageName: path.basename(stage), targetRoot: resolvedRoot, @@ -1362,38 +1375,79 @@ export function restoreLegacyBackup({ originalIno: rootIdentity?.ino.toString() ?? null, stageDev: stageStat.dev.toString(), stageIno: stageStat.ino.toString(), + }; + + // W73-01: durable discoverable recovery intent precedes every destructive + // root-directory rename. + appendRestoreTransactionRecord({ + targetRoot: resolvedRoot, + transaction, + phase: 'PREPARED', }); - // Moving the existing root out of the way does not follow a symlink. - // Verify that the directory moved is still the original preflighted inode - // before publishing the staged replacement. if (originalRootExists) { - fs.renameSync(resolvedRoot, displaced); - syncRestoreParent(parent); + durableRestoreRename(resolvedRoot, displaced); displacedOriginal = true; - const moved = fs.lstatSync(displaced); - if (moved.dev !== rootStat.dev || moved.ino !== rootStat.ino) { + + const moved = fs.lstatSync(displaced, { bigint: true }); + if (!sameRestoreIdentity(moved, transaction.originalDev, transaction.originalIno)) { throw new StateMigrationError('Legacy backup restore root changed during staging'); } + appendRestoreTransactionRecord({ + targetRoot: resolvedRoot, + transaction, + phase: 'ORIGINAL_DISPLACED', + }); } - fs.renameSync(stage, resolvedRoot); - syncRestoreParent(parent); + + // W73-03: publish candidate using the platform durability primitive. + durableRestoreRename(stage, resolvedRoot); promoted = true; displacedOriginal = false; - // Preserve original root as a retained rollback asset. Only after the - // new root is durably visible should the journal be acknowledged. - fs.unlinkSync(journal); - syncRestoreParent(parent); - // Promotion is now acknowledged and durable. An old tree kept in the - // displaced slot is not needed for this completed transaction. - if (originalRootExists) { - try { fs.rmSync(displaced, { recursive: true, force: true }); } catch { - // Retain rather than damage the newly promoted root if cleanup fails. + + const published = fs.lstatSync(resolvedRoot, { bigint: true }); + if (!sameRestoreIdentity(published, transaction.stageDev, transaction.stageIno)) { + throw new StateMigrationError('Published restore candidate differs from staged identity witness'); + } + appendRestoreTransactionRecord({ + targetRoot: resolvedRoot, + transaction, + phase: 'CANDIDATE_PUBLISHED', + }); + + // Journal acknowledgement is an explicit persistent state transition, not + // deletion of the only recovery authority. + appendRestoreTransactionRecord({ + targetRoot: resolvedRoot, + transaction, + phase: 'PROMOTION_ACKNOWLEDGED', + }); + appendRestoreTransactionRecord({ + targetRoot: resolvedRoot, + transaction, + phase: RESTORE_TERMINAL_PHASE, + outcome: originalRootExists ? 'promoted-original-retained' : 'promoted-new-root', + }); + + let cleanupPending = false; + if (originalRootExists && fs.existsSync(displaced)) { + try { + fs.rmSync(displaced, { recursive: true, force: true }); + syncRestoreParent(parent); + } catch { + // Cleanup is intentionally post-terminal. Preserve the authenticated + // old root rather than turning cleanup failure into data loss. + cleanupPending = true; } } + return Object.freeze({ restoredFiles: restored.sort(), sourceFingerprint: bundle.sourceFingerprint, + transactionId, + transactionLog: restoreJournalPath(resolvedRoot), + cleanupPending, + displacedOriginal: cleanupPending && originalRootExists ? displaced : null, }); } finally { if (displacedOriginal) { @@ -1401,10 +1455,21 @@ export function restoreLegacyBackup({ if (!fs.existsSync(resolvedRoot)) { durableRestoreRollbackRename(displaced, resolvedRoot); } - } catch {} + } catch { + // Explicit recoverLegacyRestore remains the authority after any + // ambiguous/failed in-process rollback. + } } if (!promoted) { - try { fs.rmSync(stage, { recursive: true, force: true }); } catch {} + try { + if (fs.existsSync(stage)) { + fs.rmSync(stage, { recursive: true, force: true }); + syncRestoreParent(parent); + } + } catch { + // Retain orphan stage witness on cleanup failure; future restore will + // fail closed if no matching transaction log can be proven. + } } } } From 457e6ab936b71cde016bada1ce62d620281ffc63 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:38:19 +0200 Subject: [PATCH 022/177] test(v0.13): add ledger-loss and persistent restore regressions --- scripts/state-engine-v013.test.mjs | 223 +++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 scripts/state-engine-v013.test.mjs diff --git a/scripts/state-engine-v013.test.mjs b/scripts/state-engine-v013.test.mjs new file mode 100644 index 000000000..f3e01c820 --- /dev/null +++ b/scripts/state-engine-v013.test.mjs @@ -0,0 +1,223 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { + appendMetadataEvent, + getStateEnginePaths, + loadCanonicalEvents, + loadStateSnapshot, + rebuildStateIndex, + verifyStateEngineIntegrity, +} from '../runtime/orchestration/state-engine-v2.mjs'; +import { + getLegacyBackupPath, + migrateLegacyStateToV2, + recoverLegacyRestore, + restoreLegacyBackup, +} from '../runtime/orchestration/state-engine-migration.mjs'; +import { createInitialState } from '../runtime/autopilot/transition-model.mjs'; +import { saveStateRevision } from '../runtime/autopilot/state-store.mjs'; + +function tempDir(t, prefix = 'dk-v013-state-') { + const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + return root; +} + +function captureMaterialized(paths) { + return { + events: fs.readFileSync(paths.events, 'utf8'), + snapshot: fs.readFileSync(paths.snapshot, 'utf8'), + index: fs.readFileSync(paths.index, 'utf8'), + }; +} + +function restoreMaterialized(paths, captured) { + fs.writeFileSync(paths.events, captured.events, 'utf8'); + fs.writeFileSync(paths.snapshot, captured.snapshot, 'utf8'); + fs.writeFileSync(paths.index, captured.index, 'utf8'); + if (fs.existsSync(paths.pending)) fs.rmSync(paths.pending, { force: true }); +} + +function createLegacyBackupFixture(t) { + const sourceRoot = tempDir(t, 'dk-v013-legacy-source-'); + const state = createInitialState({ autonomy: 'guided-autopilot' }, sourceRoot); + saveStateRevision(state, sourceRoot); + const migration = migrateLegacyStateToV2({ + rootDir: sourceRoot, + completedAt: '2026-10-14T20:00:00.000Z', + }); + assert.equal(migration.semanticEquivalence, true); + const backupPath = getLegacyBackupPath(sourceRoot); + assert.equal(fs.existsSync(backupPath), true); + return { sourceRoot, backupPath }; +} + +test('V13-LEDGER-01 pending journal loss cannot strand an unambiguous torn trailing append', (t) => { + const rootDir = tempDir(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const baseline = captureMaterialized(paths); + + appendMetadataEvent({ rootDir, eventType: 'UNACKNOWLEDGED_EVENT', payload: { value: 2 } }); + const full = fs.readFileSync(paths.events, 'utf8'); + const suffix = full.slice(baseline.events.length); + + restoreMaterialized(paths, baseline); + const torn = baseline.events + suffix.slice(0, Math.max(1, Math.floor(suffix.length / 3))); + fs.writeFileSync(paths.events, torn, 'utf8'); + + const recovered = loadCanonicalEvents(rootDir); + assert.equal(recovered.length, 1); + assert.equal(fs.readFileSync(paths.events, 'utf8'), baseline.events); + assert.equal(fs.existsSync(paths.pending), false); +}); + +test('V13-LEDGER-02 complete valid final event missing only newline is preserved, not truncated', (t) => { + const rootDir = tempDir(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const baseline = captureMaterialized(paths); + + appendMetadataEvent({ rootDir, eventType: 'SECOND_EVENT', payload: { value: 2 } }); + const full = fs.readFileSync(paths.events, 'utf8'); + const suffix = full.slice(baseline.events.length); + assert.ok(suffix.endsWith('\n')); + + restoreMaterialized(paths, baseline); + fs.writeFileSync(paths.events, baseline.events + suffix.slice(0, -1), 'utf8'); + + const recovered = loadCanonicalEvents(rootDir); + assert.equal(recovered.length, 2); + assert.ok(fs.readFileSync(paths.events, 'utf8').endsWith('\n')); + + loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); + rebuildStateIndex(rootDir); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + +test('V13-LEDGER-03 acknowledged snapshot prevents truncating a torn tail that may contain accepted history', (t) => { + const rootDir = tempDir(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const baselineEvents = fs.readFileSync(paths.events, 'utf8'); + + appendMetadataEvent({ rootDir, eventType: 'ACKNOWLEDGED_EVENT', payload: { value: 2 } }); + const full = fs.readFileSync(paths.events, 'utf8'); + const suffix = full.slice(baselineEvents.length); + const torn = baselineEvents + suffix.slice(0, Math.max(1, Math.floor(suffix.length / 3))); + fs.writeFileSync(paths.events, torn, 'utf8'); + if (fs.existsSync(paths.pending)) fs.rmSync(paths.pending, { force: true }); + + assert.throws( + () => loadCanonicalEvents(rootDir), + /Refusing to truncate trailing canonical bytes witnessed by the materialized snapshot/, + ); + assert.equal(fs.readFileSync(paths.events, 'utf8'), torn); +}); + +test('V13-LEDGER-04 parseable hash-invalid final event fails closed even without pending journal', (t) => { + const rootDir = tempDir(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const baseline = captureMaterialized(paths); + + appendMetadataEvent({ rootDir, eventType: 'SECOND_EVENT', payload: { value: 2 } }); + const full = fs.readFileSync(paths.events, 'utf8'); + const line = full.slice(baseline.events.length).trim(); + const tampered = JSON.parse(line); + tampered.payload = { value: 999 }; + + restoreMaterialized(paths, baseline); + const bytes = baseline.events + JSON.stringify(tampered); + fs.writeFileSync(paths.events, bytes, 'utf8'); + + assert.throws(() => loadCanonicalEvents(rootDir), /hash mismatch/i); + assert.equal(fs.readFileSync(paths.events, 'utf8'), bytes); +}); + +test('V13-LEDGER-05 mid-ledger corruption remains fail-closed', (t) => { + const rootDir = tempDir(t); + appendMetadataEvent({ rootDir, eventType: 'ONE', payload: { value: 1 } }); + appendMetadataEvent({ rootDir, eventType: 'TWO', payload: { value: 2 } }); + appendMetadataEvent({ rootDir, eventType: 'THREE', payload: { value: 3 } }); + const paths = getStateEnginePaths(rootDir); + const lines = fs.readFileSync(paths.events, 'utf8').trimEnd().split('\n'); + lines[1] = '{corrupt-middle'; + const corrupted = lines.join('\n') + '\n'; + fs.writeFileSync(paths.events, corrupted, 'utf8'); + + assert.throws(() => loadCanonicalEvents(rootDir), /JSON is invalid at line 2/i); + assert.equal(fs.readFileSync(paths.events, 'utf8'), corrupted); +}); + +test('W73-01/W73-04 normal restore retains hash-chained terminal transaction evidence', (t) => { + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-restore-parent-'); + const targetRoot = path.join(parent, 'restored-project'); + + const result = restoreLegacyBackup({ + backupPath, + targetRoot, + overwrite: true, + confirmOffline: true, + }); + + assert.equal(fs.existsSync(result.transactionLog), true); + assert.equal(fs.existsSync(targetRoot), true); + const lines = fs.readFileSync(result.transactionLog, 'utf8').trimEnd().split('\n').map(JSON.parse); + assert.deepEqual( + lines.map((record) => record.phase), + ['PREPARED', 'CANDIDATE_PUBLISHED', 'PROMOTION_ACKNOWLEDGED', 'CLEANUP_SAFE'], + ); + assert.ok(lines.every((record) => /^sha256:[a-f0-9]{64}$/.test(record.recordHash))); + assert.equal(lines.at(-1).outcome, 'promoted-new-root'); + + const recovery = recoverLegacyRestore({ targetRoot, confirmOffline: true }); + assert.equal(recovery.alreadyComplete, true); + assert.equal(recovery.transactionId, result.transactionId); +}); + +test('W73-09 a second restore appends a new transaction without deleting prior recovery history', (t) => { + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-restore-repeat-'); + const targetRoot = path.join(parent, 'restored-project'); + + const first = restoreLegacyBackup({ backupPath, targetRoot, overwrite: true, confirmOffline: true }); + const firstLog = fs.readFileSync(first.transactionLog, 'utf8'); + + const second = restoreLegacyBackup({ backupPath, targetRoot, overwrite: true, confirmOffline: true }); + assert.equal(second.transactionLog, first.transactionLog); + const secondLog = fs.readFileSync(second.transactionLog, 'utf8'); + assert.ok(secondLog.startsWith(firstLog)); + + const records = secondLog.trimEnd().split('\n').map(JSON.parse); + const transactions = [...new Set(records.map((record) => record.transactionId))]; + assert.equal(transactions.length, 2); + assert.equal(records.at(-1).phase, 'CLEANUP_SAFE'); +}); + +test('W73-07 missing journal with a surviving staging witness fails closed instead of guessing', (t) => { + const parent = tempDir(t, 'dk-v013-orphan-'); + const targetRoot = path.join(parent, 'project'); + const orphan = path.join(parent, '.dk-legacy-restore-stage-' + 'a'.repeat(32)); + fs.mkdirSync(orphan); + + assert.throws( + () => recoverLegacyRestore({ targetRoot, confirmOffline: true }), + /witnesses exist without a recovery journal/i, + ); + assert.equal(fs.existsSync(orphan), true); +}); + +test('W73-10 no transaction and no witness reports no recovery work', (t) => { + const parent = tempDir(t, 'dk-v013-empty-recovery-'); + const targetRoot = path.join(parent, 'project'); + assert.deepEqual( + recoverLegacyRestore({ targetRoot, confirmOffline: true }), + { recovered: false }, + ); +}); From 0c18dd841902f131a228e1ee9192a38734fdd71b Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:39:14 +0200 Subject: [PATCH 023/177] docs(v0.13): record binding T04 release authority --- .../dkf-v0.13-t04-release-authority.md | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/04-architecture/dkf-v0.13-t04-release-authority.md diff --git a/docs/04-architecture/dkf-v0.13-t04-release-authority.md b/docs/04-architecture/dkf-v0.13-t04-release-authority.md new file mode 100644 index 000000000..8321f5ee1 --- /dev/null +++ b/docs/04-architecture/dkf-v0.13-t04-release-authority.md @@ -0,0 +1,100 @@ +# Development Kit v0.13.0 — T04 Release Authority + +**Status:** Binding implementation context +**Date:** 14 October 2026 +**Current released baseline:** v0.12.1 +**Deferred increment:** DKF120-T04 — State Engine V2 +**v0.13 implementation identity:** DKF130-T04 + +## 1. Release-line decision + +Development Kit v0.12.0 deliberately shipped without State Engine V2. + +The v0.12 release retained the stable v0.11-style persistence authority and shipped T01–T03 plus T05–T10. The following State Engine V2 criteria were explicitly deferred rather than counted as passed: + +- `DKF-120-AC-012` +- `DKF-120-AC-013` +- `DKF-120-AC-014` +- `DKF-120-AC-015` +- `DKF-120-AC-034` + +The repository changelog records T04 State Engine V2 as the **highest-priority v0.13.0** work. + +## 2. Product Owner authority + +GitHub Issue #82 is the binding Product Owner release-scope decision for v0.12. + +Its effect on v0.13 is: + +- T04 was deferred, not cancelled; +- the original T04 reliability intent remains; +- full Windows legacy-backup restore/recovery remains required; +- risk-3 independent code, architecture and security review remains required; +- T04 may not be represented as accepted merely because implementation tests pass. + +## 3. Windows blocker + +GitHub Issue #73 remains the blocking Windows durability/recovery issue. + +The accepted direction requires: + +- no blanket Windows restore disable; +- a documented supported Windows filesystem/API contract; +- preservation of recovery evidence when durability cannot be proven; +- deterministic crash-boundary/fault-injection coverage; +- Windows and Ubuntu exact-source validation; +- fresh code, architecture and security review before acceptance. + +The current v0.13 design treats successful durability-related syscalls as API evidence, not as universal hardware power-loss certification. + +## 4. Compatibility baseline + +v0.12.1 is the integration baseline. + +T04 implementation must preserve released behavior for: + +- Cost Observatory; +- Workspace Targets; +- Execution Capsules and Context Cache; +- Lifecycle Instances; +- Discovery and Controlled Re-entry; +- Gate Profiles; +- Verification Extension SDK; +- Complexity Delta Guard; +- Control Center v0.12 projections; +- Design Authority; +- Secrets & Configuration Readiness; +- Development Modes; +- IDEA authority; +- deterministic acceptance and independent verification. + +State Engine V2 replaces persistence authority under these released interfaces. It does not roll their behavior back to historical T04 branch versions. + +## 5. Historical T04 branches + +Historical T04 branches and PRs are evidence/source material only: + +- original T04 implementation/validation; +- T04 reliability-hardening work; +- Windows durability investigations; +- audit/review findings. + +They are not accepted release authority and must not be merged wholesale over v0.12.1. + +v0.13 implementation starts from current `main`, selectively ports proven mechanisms, resolves the outstanding blockers and re-runs independent acceptance. + +## 6. Acceptance boundary + +T04 is not complete until the exact reviewed source has: + +- applicable AC verification PASS; +- Ubuntu validation PASS; +- Windows validation PASS; +- code review PASS; +- architecture review PASS; +- security review PASS; +- required security control-domain PASS; +- no unresolved in-scope major/critical review findings; +- deterministic acceptance computed from source-bound evidence. + +Only then may the project proceed to v0.13 release integration. From dcd48191d092e93e690c1cadc155186d146885a4 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:40:04 +0200 Subject: [PATCH 024/177] feat(v0.13): add source-bound T04 Development Contract --- scripts/v013-state-engine-v2-contract.mjs | 220 ++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 scripts/v013-state-engine-v2-contract.mjs diff --git a/scripts/v013-state-engine-v2-contract.mjs b/scripts/v013-state-engine-v2-contract.mjs new file mode 100644 index 000000000..e4d10a45f --- /dev/null +++ b/scripts/v013-state-engine-v2-contract.mjs @@ -0,0 +1,220 @@ +#!/usr/bin/env node + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + checkContractStaleness, + ensureDevelopmentContract, + validateDevelopmentContract, +} from '../runtime/orchestration/development-contract.mjs'; +import { resolveContractGateProfiles } from '../runtime/orchestration/gate-profiles.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +const task = { + id: 'DKF130-T04', + projectId: 'proj_cad505c1-901d-46f0-a0d1-297598bdae18', + status: 'approved', + objective: 'Complete the deferred State Engine V2 increment on top of released v0.12.1, including canonical event-backed state, verified legacy migration, bounded state-file growth, and full Windows legacy-backup restore/recovery with persistent crash-recovery witnesses.', + scope: { + in: [ + 'runtime/orchestration/state-engine-v2.mjs', + 'runtime/orchestration/state-engine-migration.mjs', + 'runtime/autopilot/state-store.mjs', + 'runtime/orchestration/orchestration-run.mjs', + 'runtime/orchestration/index.mjs', + 'schemas/state-engine-*.schema.json', + 'scripts/windows-durable-rename.ps1', + 'scripts/state-engine-v2*.test.mjs', + 'scripts/state-engine-v013.test.mjs', + 'scripts/state-engine-v2-benchmark.mjs', + 'scripts/v013-state-engine-v2-contract.mjs', + '.github/workflows/ci.yml', + 'package.json', + 'docs/', + 'memory.md', + ], + out: [ + 'Application-specific or industry-specific state semantics', + 'Changing released Lifecycle Instance semantics', + 'Changing Discovery and Controlled Re-entry semantics', + 'Weakening Gate Profiles or Verification Extension acceptance', + 'Distributed cross-host locking', + 'Same-account hostile writer hardening tracked separately from Issue #73', + 'Publishing v0.13.0', + 'Deleting retained legacy state before a later explicit cleanup policy', + ], + }, + requirements: [ + 'DKF-120-AC-012: canonical development-state history is append-only, deterministically ordered and integrity chained.', + 'DKF-120-AC-013: materialized current state rebuilds deterministically from canonical history.', + 'DKF-120-AC-014: acceleration index deletion or corruption is rebuildable without semantic change.', + 'DKF-120-AC-015: supported legacy state migration is idempotent, fail-closed, recoverable and semantically equivalent before cutover.', + 'DKF-120-AC-034: canonical transition-state file growth is bounded rather than proportional to normal transition count.', + 'Preserve v0.12.1 public command and T01-T03/T05-T10 behavior while replacing persistence authority underneath released interfaces.', + 'A missing Windows pending-commit journal must not make an unambiguous torn trailing canonical append unrecoverable.', + 'Windows restore must retain persistent transaction witnesses and support deterministic recovery on the declared supported storage profile.', + 'Unsupported Windows storage profiles must fail before destructive target-root mutation and preserve recoverable assets.', + 'Independent code, architecture and security review remain required for risk-3 acceptance.', + ], + acceptanceCriteria: [ + { + id: 'DKF-120-AC-012', + statement: 'State history is append-only and canonical, with deterministic ordering and integrity chaining.', + source: 'DKF-120-AC-012', + verificationType: ['test'], + requiredEvidence: true, + }, + { + id: 'DKF-120-AC-013', + statement: 'Current state is materialized from canonical history and can be rebuilt deterministically.', + source: 'DKF-120-AC-013', + verificationType: ['test'], + requiredEvidence: true, + }, + { + id: 'DKF-120-AC-014', + statement: 'The acceleration index can be deleted or corrupted and rebuilt without changing semantic state.', + source: 'DKF-120-AC-014', + verificationType: ['test'], + requiredEvidence: true, + }, + { + id: 'DKF-120-AC-015', + statement: 'Supported legacy state migrates idempotently, fail-closed, recoverably and semantically equivalently before cutover.', + source: 'DKF-120-AC-015', + verificationType: ['test', 'security'], + requiredEvidence: true, + }, + { + id: 'DKF-120-AC-034', + statement: 'Canonical transition-state file growth is bounded and no longer proportional to normal transition count.', + source: 'DKF-120-AC-034', + verificationType: ['test', 'architecture-verification'], + requiredEvidence: true, + }, + ], + architectureConstraints: [ + 'events.jsonl is canonical; snapshot.json is materialized/rebuildable; index.db is disposable acceleration only.', + 'SQLite may not become the sole source of truth.', + 'Fresh projects may use V2 directly; legacy projects may cut over only after verified migration-complete evidence.', + 'Historical v0.12 T04 branches are source material only and may not overwrite later v0.12.1 feature semantics.', + 'State Engine V2 remains generic DKF development-state infrastructure.', + 'The canonical ledger may automatically repair only unambiguous final append damage; mid-history or hash-chain ambiguity fails closed.', + ], + designConstraints: [], + securityConstraints: [ + 'No symlink, junction, path traversal or hard-link alias may redirect canonical state or restore writes outside approved boundaries.', + 'Recovery may never destroy the last authenticated recoverable copy.', + 'Windows restore requires positive supported-storage identification before destructive root mutation.', + 'Required Windows durability helper failures must propagate and may not silently fall back to plain rename.', + 'Pending journals and restore transaction records are recovery witnesses and must not be treated as advisory summaries.', + ], + risk: { + level: 3, + reasons: [ + 'Persistent-state migration and recovery are control-plane critical.', + 'Incorrect durability or cutover could lose or falsely advance authoritative development state.', + 'Windows restore mutates project-root directory entries and requires fail-closed recovery semantics.', + ], + }, + requiredVerification: ['tests'], + requiredReviewers: ['code-reviewer'], + correctionPolicy: { maxAttempts: 3 }, + approvalPolicy: { + requiredGateProfileIds: ['gp-high-assurance'], + }, + executionSafety: { + resourceScope: 'project-only', + destructiveOperations: 'explicit-approval', + remoteMutation: 'forbidden', + }, + configurationDependencies: [], + targetBinding: { + primary: 'dkf-framework', + affected: ['dkf-framework'], + verification: ['dkf-framework'], + }, +}; + +const authoritativeSources = [ + { + path: 'docs/04-architecture/dkf-v0.13-state-engine-v2-specification.md', + kind: 'specification', + authority: 'required', + sections: ['3. Required canonical layout', '4. Acceptance criteria restored for v0.13', '6. Runtime commit durability contract', '7. Windows legacy-backup restore/recovery contract', '11. Required independent gates'], + }, + { + path: 'docs/04-architecture/dkf-v0.13-t04-release-authority.md', + kind: 'product-owner-release-authority', + authority: 'required', + sections: ['1. Release-line decision', '2. Product Owner authority', '3. Windows blocker', '6. Acceptance boundary'], + }, + { + path: 'CHANGELOG.md', + kind: 'released-baseline', + authority: 'required', + sections: ['0.12.1', '0.12.0'], + }, + { + path: 'docs/04-architecture/dkf-scale-context-iteration-v0.12-technical-design.md', + kind: 'historical-technical-design', + authority: 'supporting', + sections: ['7. State Engine V2'], + }, + { + path: 'docs/04-architecture/dkf-scale-context-iteration-v0.12-implementation-plan.md', + kind: 'historical-plan', + authority: 'supporting', + sections: ['6. Task DKF120-T04 — State Engine V2'], + }, +]; + +const result = ensureDevelopmentContract({ + rootDir: ROOT, + projectId: task.projectId, + task, + authoritativeSources, + contractId: 'INC-DKF130-T04', +}); + +validateDevelopmentContract(result.contract); +if (!result.contract.workspaceTargets || result.contract.workspaceTargets.primary !== 'dkf-framework') { + throw new Error('v0.13 T04 must use the dkf-framework target-aware Development Contract'); +} + +const gates = resolveContractGateProfiles(result.contract, { + securitySensitive: true, + architectureSensitive: true, +}); +const expectedVerification = ['architecture-verification', 'security-audit', 'specification', 'tests']; +const expectedReviewers = ['architecture-reviewer', 'code-reviewer', 'security-reviewer']; +if (JSON.stringify(gates.verification) !== JSON.stringify(expectedVerification)) { + throw new Error('Unexpected v0.13 T04 verification gates: ' + gates.verification.join(', ')); +} +if (JSON.stringify(gates.reviewers) !== JSON.stringify(expectedReviewers)) { + throw new Error('Unexpected v0.13 T04 reviewer gates: ' + gates.reviewers.join(', ')); +} +if (JSON.stringify(gates.controlDomains) !== JSON.stringify(['security'])) { + throw new Error('Unexpected v0.13 T04 control domains: ' + gates.controlDomains.join(', ')); +} +if (gates.humanApprovals.length > 0) { + throw new Error('v0.13 T04 unexpectedly derived a human approval gate'); +} + +const staleness = checkContractStaleness(result.contract, ROOT); +if (staleness.stale) throw new Error('DKF130-T04 Development Contract is stale immediately after creation'); + +process.stdout.write(JSON.stringify({ + contractId: result.contract.contractId, + taskId: result.contract.taskId, + schemaVersion: result.contract.schemaVersion, + created: result.created, + sourceFingerprint: result.contract.sourceFingerprint, + acceptanceCriteria: result.contract.acceptanceCriteria.map((criterion) => criterion.id), + gates, + riskLevel: result.contract.risk.level, + targetBinding: result.contract.workspaceTargets, + stale: staleness.stale, +}, null, 2) + '\n'); From be2c026a30536293555747d9d95fda89dae8ff49 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:40:27 +0200 Subject: [PATCH 025/177] feat(v0.13): expose State Engine V2 runtime APIs --- runtime/orchestration/index.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/orchestration/index.mjs b/runtime/orchestration/index.mjs index 180472827..c6434dbc4 100644 --- a/runtime/orchestration/index.mjs +++ b/runtime/orchestration/index.mjs @@ -158,3 +158,6 @@ export * from './discovery-reentry.mjs'; export * from './gate-profiles.mjs'; export * from './verification-extension-sdk.mjs'; export * from './complexity-delta.mjs'; + +export * from './state-engine-v2.mjs'; +export * from './state-engine-migration.mjs'; From 8bd1dd5ebf32a0494ffe5e75f5e7e6020df839b1 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:40:30 +0200 Subject: [PATCH 026/177] test(v0.13): add State Engine validation lane --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e07a7f081..aa4b481a5 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,10 @@ "complexity-delta:test": "node --test scripts/complexity-delta.test.mjs", "control-center-v012:test": "node --test scripts/control-center-v012.test.mjs", "release-scope:v012": "node --test scripts/v012-release-scope.test.mjs", - "consumer-smoke:v012": "node scripts/v012-consumer-smoke.mjs" + "consumer-smoke:v012": "node scripts/v012-consumer-smoke.mjs", + "v013-state-engine-v2:test": "node scripts/v013-state-engine-v2-contract.mjs && node --test scripts/state-engine-v2.test.mjs scripts/state-engine-v2-migration.test.mjs scripts/state-engine-v2-hardening.test.mjs scripts/state-engine-v013.test.mjs scripts/t04-review-thread-blockers.test.mjs", + "state-engine-v2:benchmark": "node scripts/state-engine-v2-benchmark.mjs --events 300", + "v013:regression": "npm run validate && npm run skills:validate:test && npm run doctor && npm run sync:validate:test && npm run docs:validate && npm run docs:validate:test && npm run opencode:validate && npm run platform:validate && npm run research:validate && npm run next-step:test && npm run installer:validate:test && npm run git-bootstrap:test && npm run orchestration:validate && npm run execution-safety:validate && npm run evidence:validate && npm run orchestration-core:validate && npm run orchestration-integration:validate && npm run v09-reliability:validate && npm run intelligence:validate && npm run v071:validate && npm run ui-preview:test && npm run token-efficiency:test && npm run token:audit:check && npm run cost-observatory:test && npm run workspace-targets:test && npm run execution-capsule:test && npm run design-authority:validate && npm run autopilot:validate && npm run numbered-decisions:test && npm run configuration-readiness:validate && npm run development-modes:validate && npm run idea-authority:validate && npm run lifecycle-instances:test && npm run discovery-reentry:test && npm run gate-profiles:test && npm run verification-extension-sdk:test && npm run complexity-delta:test && npm run control-center-v012:test && npm run v013-state-engine-v2:test" }, "engines": { "node": ">=18.0.0" From 7f9255d6f5e9262faeef9b3ce019d8fbb066cc85 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:40:42 +0200 Subject: [PATCH 027/177] ci(v0.13): add State Engine V2 validation lane --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1db9786ba..446d1f242 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,8 +99,13 @@ jobs: run: npm run execution-capsule:test - name: Validate v0.12 reduced-release scope + if: ${{ github.head_ref != 'feature/v0.13-state-engine-v2' }} run: npm run release-scope:v012 + - name: Validate v0.13 State Engine V2 increment + if: ${{ github.head_ref == 'feature/v0.13-state-engine-v2' }} + run: npm run v013-state-engine-v2:test + - name: Validate v0.12 non-T04 feature increments run: | npm run lifecycle-instances:test @@ -120,7 +125,13 @@ jobs: run: npm pack --dry-run - name: Verify installed v0.12 consumer package + if: ${{ github.head_ref != 'feature/v0.13-state-engine-v2' }} run: npm run consumer-smoke:v012 - name: Validate exact release command + if: ${{ github.head_ref != 'feature/v0.13-state-engine-v2' }} run: npm run release:validate + + - name: Validate v0.13 full compatibility regression + if: ${{ github.head_ref == 'feature/v0.13-state-engine-v2' }} + run: npm run v013:regression From c2a6fd6137feedeb261fc8884035a29cd10c551c Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:41:02 +0200 Subject: [PATCH 028/177] docs(v0.13): document T04 contract generator --- .../scripts/v013-state-engine-v2-contract.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/03-reference/scripts/v013-state-engine-v2-contract.md diff --git a/docs/03-reference/scripts/v013-state-engine-v2-contract.md b/docs/03-reference/scripts/v013-state-engine-v2-contract.md new file mode 100644 index 000000000..290f93e2f --- /dev/null +++ b/docs/03-reference/scripts/v013-state-engine-v2-contract.md @@ -0,0 +1,83 @@ +# v013-state-engine-v2-contract.mjs + +## Purpose + +`scripts/v013-state-engine-v2-contract.mjs` creates or reuses the Development Contract for **DKF130-T04 — State Engine V2**, the first v0.13 priority carried forward from deferred DKF120-T04. + +The runtime contract is persisted under: + +```text +.development-kit/contracts/INC-DKF130-T04/ +``` + +The directory remains git-ignored. The committed generator makes contract construction reproducible from the exact v0.13 authority files. + +## Bound authority + +The contract fingerprints: + +- `dkf-v0.13-state-engine-v2-specification.md`; +- `dkf-v0.13-t04-release-authority.md`; +- the released v0.12.1/v0.12.0 changelog baseline; +- the historical v0.12 State Engine technical design and PLAN as supporting context. + +## Restored acceptance criteria + +- `DKF-120-AC-012` +- `DKF-120-AC-013` +- `DKF-120-AC-014` +- `DKF-120-AC-015` +- `DKF-120-AC-034` + +These retain their original IDs because v0.12 explicitly deferred rather than replaced them. + +## High-assurance gates + +The contract is risk level 3 and binds the high-assurance Gate Profile. + +Expected effective gates: + +```text +verification: + architecture-verification + security-audit + specification + tests + +reviewers: + architecture-reviewer + code-reviewer + security-reviewer + +control domains: + security +``` + +T04 cannot be accepted from tests alone. + +## Target binding + +```text +primary: dkf-framework +affected: dkf-framework +verification: dkf-framework +``` + +## Usage + +```bash +node scripts/v013-state-engine-v2-contract.mjs +``` + +The generator validates source freshness, target freshness and effective gates immediately. + +## Boundary + +The contract does not authorize: + +- application/domain-specific state policy; +- distributed locking; +- Issue #72 hostile same-account writer hardening; +- removal of v0.12.1 features; +- release publication; +- deletion of retained legacy recovery assets. From e7d444aba0c33741c3684c8b5569f69ea7f23d51 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:41:05 +0200 Subject: [PATCH 029/177] docs(v0.13): index State Engine authority and contract reference --- docs/SUMMARY.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index fed5dfcee..615e8c88c 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -195,6 +195,7 @@ * [phase03-cost-observatory-contract](03-reference/scripts/phase03-cost-observatory-contract.md) * [phase04-workspace-target-engine-contract](03-reference/scripts/phase04-workspace-target-engine-contract.md) * [phase05-execution-capsule-contract](03-reference/scripts/phase05-execution-capsule-contract.md) +* [v013-state-engine-v2-contract](03-reference/scripts/v013-state-engine-v2-contract.md) * [sync-plugin](03-reference/scripts/sync-plugin.md) * [ui-preview](03-reference/scripts/ui-preview.md) * [token-audit](03-reference/scripts/token-audit.md) @@ -242,6 +243,8 @@ * [DKF Scale, Context & Iteration — Phase 0/1 Baseline](04-architecture/dkf-scale-context-iteration-phase01-baseline.md) * [DKF Scale, Context & Iteration — v0.12 Specification](04-architecture/dkf-scale-context-iteration-v0.12-specification.md) * [DKF Scale, Context & Iteration — v0.12 Technical Design](04-architecture/dkf-scale-context-iteration-v0.12-technical-design.md) +* [DKF v0.13 State Engine V2 Specification](04-architecture/dkf-v0.13-state-engine-v2-specification.md) +* [DKF v0.13 T04 Release Authority](04-architecture/dkf-v0.13-t04-release-authority.md) * [DKF Scale, Context & Iteration — v0.12 Implementation PLAN](04-architecture/dkf-scale-context-iteration-v0.12-implementation-plan.md) * [DKF v0.12 — PO-approved Release Scope (T04 deferred)](04-architecture/dkf-v0.12-release-scope-amendment.md) * [DKF v0.12 — Matched Comparative Cost and Context Evidence](04-architecture/dkf-v0.12-release-comparative-evidence.md) From 3bdc4ae14e427f6d5f30636ee9d1c9672003277a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:42:13 +0200 Subject: [PATCH 030/177] docs(v0.13): add State Engine script references --- .../scripts/state-engine-v2-benchmark.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/03-reference/scripts/state-engine-v2-benchmark.md diff --git a/docs/03-reference/scripts/state-engine-v2-benchmark.md b/docs/03-reference/scripts/state-engine-v2-benchmark.md new file mode 100644 index 000000000..12d1befa5 --- /dev/null +++ b/docs/03-reference/scripts/state-engine-v2-benchmark.md @@ -0,0 +1,30 @@ +# state-engine-v2-benchmark.mjs + +## Purpose + +A reproducible, disposable **v0.13 State Engine V2 endurance fixture** that checks canonical event append and history reconstruction without modifying an existing project. + +The script creates a temporary project, appends generic entity revisions, verifies the monotonic canonical hash chain and final entity state, then removes the fixture. + +## Usage + +```bash +npm run state-engine-v2:benchmark +node scripts/state-engine-v2-benchmark.mjs --events 1000 +``` + +The bounded CI/review fixture uses 300 events. The CLI accepts 1–5,000 events for targeted measurements. + +## Reported observations + +The script emits `T04_ENDURANCE_EVIDENCE` JSON including operation count, platform/Node version, append duration, integrity-read duration, final canonical-ledger bytes and modeled write savings against the former full-ledger-rewrite design. + +The historical rewrite comparison is a deterministic calculation from observed ledger lengths, not a measured execution of the retired implementation and not a release-wide performance claim. + +## Authority and safety + +- `events.jsonl` remains canonical. +- Snapshot/index are rebuildable. +- Interrupted appends are recovery-gated. +- The fixture uses a disposable temporary project. +- Passing the benchmark does not satisfy independent risk-3 acceptance. From 2a214e8584b6a9f54350c967cc878054e1a9aa4c Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:42:16 +0200 Subject: [PATCH 031/177] docs(v0.13): add State Engine script references --- .../scripts/t04-review-thread-blockers.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/03-reference/scripts/t04-review-thread-blockers.md diff --git a/docs/03-reference/scripts/t04-review-thread-blockers.md b/docs/03-reference/scripts/t04-review-thread-blockers.md new file mode 100644 index 000000000..e4fc672e2 --- /dev/null +++ b/docs/03-reference/scripts/t04-review-thread-blockers.md @@ -0,0 +1,28 @@ +# t04-review-thread-blockers.mjs + +## Purpose + +Provides deterministic helpers for the **v0.13 T04 risk-3 review gate**. + +T04 acceptance must refuse approval while an authenticated review thread remains unresolved, while source anchoring is ambiguous, or while authenticated review evidence is malformed. + +## Usage + +The module is imported by T04 acceptance/review tooling and is not itself an acceptance authority. + +```js +import { unresolvedReviewThreads } from './t04-review-thread-blockers.mjs'; +const blockers = unresolvedReviewThreads(githubReviewThreads); +``` + +The helper also validates canonical review-request source SHA anchoring and authenticated review-thread page structure. + +## Verification + +```bash +node --test scripts/t04-review-thread-blockers.test.mjs +``` + +The regressions cover unresolved findings, conflicting or duplicated source SHAs, malformed review evidence and pagination failures. + +This helper cannot generate reviewer PASS results, resolve comments, or bypass source-bound independent review. From 46c4ee630419f1b9d21213d59f759a29c590c943 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Wed, 14 Oct 2026 22:42:19 +0200 Subject: [PATCH 032/177] docs(v0.13): index State Engine executable references --- docs/SUMMARY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 615e8c88c..3ad8e45f1 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -196,6 +196,8 @@ * [phase04-workspace-target-engine-contract](03-reference/scripts/phase04-workspace-target-engine-contract.md) * [phase05-execution-capsule-contract](03-reference/scripts/phase05-execution-capsule-contract.md) * [v013-state-engine-v2-contract](03-reference/scripts/v013-state-engine-v2-contract.md) +* [state-engine-v2-benchmark](03-reference/scripts/state-engine-v2-benchmark.md) +* [t04-review-thread-blockers](03-reference/scripts/t04-review-thread-blockers.md) * [sync-plugin](03-reference/scripts/sync-plugin.md) * [ui-preview](03-reference/scripts/ui-preview.md) * [token-audit](03-reference/scripts/token-audit.md) From 37d1d22d94d2fdf005c35bb1d807c059883fc50b Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 08:43:23 +0200 Subject: [PATCH 033/177] fix(v0.13 T04): port CI-validated compatibility, restore witnesses and Windows alias safeguards from PR #99 --- ...dkf-v0.13-state-engine-v2-specification.md | 4 +- .../orchestration/state-engine-migration.mjs | 196 ++++++++++-- scripts/autopilot.test.mjs | 6 + scripts/lifecycle-instances.test.mjs | 6 + scripts/phase01-scale-context-baseline.mjs | 10 +- scripts/state-engine-v013.test.mjs | 298 ++++++++++++++++++ scripts/state-engine-v2-hardening.test.mjs | 16 + scripts/state-engine-v2-migration.test.mjs | 164 +++++----- scripts/t04-review-thread-blockers.mjs | 4 +- scripts/t04-review-thread-blockers.test.mjs | 7 +- scripts/v071-regression.test.mjs | 11 +- 11 files changed, 595 insertions(+), 127 deletions(-) diff --git a/docs/04-architecture/dkf-v0.13-state-engine-v2-specification.md b/docs/04-architecture/dkf-v0.13-state-engine-v2-specification.md index 8f2132393..796ec8afd 100644 --- a/docs/04-architecture/dkf-v0.13-state-engine-v2-specification.md +++ b/docs/04-architecture/dkf-v0.13-state-engine-v2-specification.md @@ -185,7 +185,7 @@ Instead: - append/replace the transaction state with an explicit terminal phase; - retain enough witness material for restart reconciliation; -- cleanup of old transaction records is a separate bounded maintenance operation, not part of the critical promotion acknowledgement. +- cleanup of old transaction records **and retained displaced-original directories** is a separate bounded, explicitly reviewed offline maintenance operation, not part of the critical promotion acknowledgement. The exact original directory inode must remain available after normal success and promoted recovery until that decision; retained data requires disk-capacity and access-control management. ### 7.2 Crash ambiguity rule @@ -230,7 +230,7 @@ The final suite MUST include Windows-specific, non-skipped coverage equivalent t - W73-02 original displacement boundary; - W73-03 candidate publication boundary; - W73-04 promotion acknowledgement boundary; -- W73-05 displaced-original cleanup boundary; +- W73-05 retained displaced-original witness and separately approved offline cleanup boundary; - W73-06 interrupted rollback and repeat recovery; - W73-07 invalid/tampered witness; - W73-08 unsupported persistence profile fails safely before destructive mutation; diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index ba56fff2b..57b20b7db 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -343,7 +343,15 @@ function orchestrationHistories(rootDir) { } } - if (states.length === 0) states = [manifest]; + if (states.length === 0) { + // Fresh v0.13 runs retain immutable manifests but persist revisions in + // the canonical V2 ledger. A manifest without legacy revision files + // is not a legacy revision-1 history when that run already exists in V2. + const canonicalRun = loadEntityState('orchestration-run', + `${manifest.contractId}/${manifest.runId}`, rootDir); + if (canonicalRun) continue; + states = [manifest]; + } const current = states.at(-1); const finalPath = path.join(runDir, 'final-state.json'); @@ -631,7 +639,7 @@ function validateRestoreRecord(record, previous = null) { if (typeof record.targetRoot !== 'string' || path.resolve(record.targetRoot) !== record.targetRoot) { throw new StateMigrationError('Restore transaction targetRoot is invalid'); } - if (typeof record.stageName !== 'string' || !/^\.dk-legacy-restore-stage-[a-f0-9]{32}$/.test(record.stageName)) { + if (typeof record.stageName !== 'string' || !/^\.dk-legacy-restore-stage-(?:[a-f0-9]{24}-)?[a-f0-9]{32}$/.test(record.stageName)) { throw new StateMigrationError('Restore transaction stageName is invalid'); } if (record.existed !== true && record.existed !== false) { @@ -662,9 +670,22 @@ function validateRestoreRecord(record, previous = null) { return true; } +function inspectSafeRestoreJournal(journal) { + let stat; + try { stat = fs.lstatSync(journal, { bigint: true }); } + catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } + if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n) { + throw new StateMigrationError('Unsafe restore transaction journal link or object type; refusing filesystem mutation'); + } + return stat; +} + function loadRestoreJournal(targetRoot) { const journal = restoreJournalPath(targetRoot); - if (!fs.existsSync(journal)) return []; + if (!inspectSafeRestoreJournal(journal)) return []; const content = fs.readFileSync(journal, 'utf8'); if (!content) return []; if (!content.endsWith('\n')) { @@ -730,9 +751,18 @@ function appendRestoreTransactionRecord({ const record = { ...base, recordHash: restoreRecordHash(base) }; fs.mkdirSync(path.dirname(journal), { recursive: true }); - const existed = fs.existsSync(journal); - const fd = fs.openSync(journal, 'a', 0o600); + const prior = inspectSafeRestoreJournal(journal); + const existed = prior !== null; + const noFollow = process.platform !== 'win32' ? (fs.constants.O_NOFOLLOW || 0) : 0; + const flags = fs.constants.O_WRONLY | fs.constants.O_APPEND | noFollow | + (existed ? 0 : fs.constants.O_CREAT | fs.constants.O_EXCL); + const fd = fs.openSync(journal, flags, 0o600); try { + const opened = fs.fstatSync(fd, { bigint: true }); + if (!opened.isFile() || opened.nlink !== 1n || + (prior && (opened.dev !== prior.dev || opened.ino !== prior.ino))) { + throw new StateMigrationError('Restore transaction journal changed identity before append'); + } fs.writeFileSync(fd, JSON.stringify(record) + '\n', 'utf8'); fs.fsyncSync(fd); } finally { @@ -749,13 +779,92 @@ function orphanRestoreWitnesses(targetRoot) { const root = path.resolve(targetRoot); const parent = path.dirname(root); if (!fs.existsSync(parent)) return []; + const scopedPrefix = '.dk-legacy-restore-stage-' + restoreJournalDigest(root) + '-'; return fs.readdirSync(parent) - .filter((name) => /^\.dk-legacy-restore-stage-[a-f0-9]{32}(?:-original)?$/.test(name)) + .filter((name) => + // New v0.13 stages are bound to this exact target; retained originals + // belonging to another sibling project must not block its restore. + (name.startsWith(scopedPrefix) && + /^[a-f0-9]{32}(?:-original)?$/.test(name.slice(scopedPrefix.length))) || + // Pre-scoped witnesses cannot be safely attributed without a journal. + // Keep the historical conservative fail-closed behavior for those. + /^\.dk-legacy-restore-stage-[a-f0-9]{32}(?:-original)?$/.test(name)) .map((name) => path.join(parent, name)) .sort(); } +/** + * Windows path lookup is ordinarily case-insensitive, but the historical + * transaction-log digest is based on the caller's exact path spelling. + * When a published root is missing after a crash, realpath cannot recover + * its original casing. Inspect bounded first-record witnesses in sibling + * journals before treating a differently cased target as a fresh project. + * + * This is a fail-closed alias guard, not proof of physical power-loss + * durability or a defence against a hostile same-account filesystem writer. + */ +function assertNoWindowsCaseAliasedRestoreJournal(targetRoot) { + if (process.platform !== 'win32') return; + const root = path.resolve(targetRoot); + const parent = path.dirname(root); + if (!fs.existsSync(parent)) return; + const expectedJournal = path.basename(restoreJournalPath(root)).toLowerCase(); + const targetKey = root.normalize('NFC').toLowerCase(); + for (const name of fs.readdirSync(parent)) { + if (!/^\.dk-restore-transaction-[a-f0-9]{24}\.jsonl$/i.test(name) || + name.toLowerCase() === expectedJournal) continue; + const candidate = path.join(parent, name); + const stat = fs.lstatSync(candidate); + if (!stat.isFile() || stat.isSymbolicLink()) continue; + const fd = fs.openSync(candidate, 'r'); + let firstLine; + try { + const bytes = Buffer.alloc(8192); + const length = fs.readSync(fd, bytes, 0, bytes.length, 0); + firstLine = bytes.subarray(0, length).toString('utf8').split('\n', 1)[0]; + } finally { + fs.closeSync(fd); + } + let witness; + try { witness = JSON.parse(firstLine); } + catch { continue; } + if (typeof witness?.targetRoot !== 'string' || + path.resolve(witness.targetRoot).normalize('NFC').toLowerCase() !== targetKey) continue; + validateRestoreRecord(witness); + throw new StateMigrationError( + 'Terminal restore root witness exists under a Windows case alias; refusing destructive mutation', + { code: 'RESTORE_CASE_ALIAS_WITNESS', journal: candidate }, + ); + } +} + +/** + * A Windows journal may disappear from the durable namespace even when its + * scoped stage/displaced-original directory survived. The stage digest then + * cannot be attributed safely to a differently cased missing target root. + * Reject a new restore anywhere in that parent until the orphan is recovered + * by an operator; do not silently treat a sibling name as proof of safety. + */ +function assertNoWindowsOrphanRestoreStages(targetRoot) { + if (process.platform !== 'win32') return; + const parent = path.dirname(path.resolve(targetRoot)); + if (!fs.existsSync(parent)) return; + for (const name of fs.readdirSync(parent)) { + const match = /^\.dk-legacy-restore-stage-([a-f0-9]{24})-[a-f0-9]{32}(?:-original)?$/i.exec(name); + if (!match) continue; + const journal = path.join(parent, '.dk-restore-transaction-' + match[1] + '.jsonl'); + if (!inspectSafeRestoreJournal(journal)) { + throw new StateMigrationError( + 'Orphan restore witnesses exist without a recovery journal; refusing destructive mutation', + { code: 'RESTORE_WITNESS_WITHOUT_JOURNAL', witness: path.join(parent, name) }, + ); + } + } +} + function assertNoUnjournaledRestoreWitness(targetRoot) { + assertNoWindowsCaseAliasedRestoreJournal(targetRoot); + assertNoWindowsOrphanRestoreStages(targetRoot); if (fs.existsSync(restoreJournalPath(targetRoot)) || fs.existsSync(legacyRestoreJournalPath(targetRoot))) return; const witnesses = orphanRestoreWitnesses(targetRoot); if (witnesses.length > 0) { @@ -1054,6 +1163,28 @@ function recoverLegacyRestoreV1({ root, parent, journalPath }) { }); } +function requireTerminalRestoreRootWitness(root, transaction, rawOutcome) { + // Historical V1 journals are imported with a legacy-v1- outcome prefix. + // Normalize only that authenticated import marker; all other outcomes must + // match the closed terminal outcome set and the exact on-disk root inode. + const outcome = typeof rawOutcome === 'string' && rawOutcome.startsWith('legacy-v1-') + ? rawOutcome.slice('legacy-v1-'.length) : rawOutcome; + const rootWitness = inspectRestorePath(root); + const promoted = outcome === 'promoted-new-root' || outcome === 'promoted-original-retained'; + const original = outcome === 'rolled-back-original' || outcome === 'uncommitted-original-unchanged'; + const witnessValid = promoted + ? sameRestoreIdentity(rootWitness, transaction.stageDev, transaction.stageIno) + : original + ? transaction.existed + ? sameRestoreIdentity(rootWitness, transaction.originalDev, transaction.originalIno) + : !rootWitness + : false; + if (!witnessValid) { + throw new StateMigrationError('Terminal restore root identity witness mismatch; refusing to supersede recovery evidence'); + } + return true; +} + /** * Recover only an interrupted OFFLINE restore for the exact target root. * Recovery is witness-driven and never treats absence of a journal entry as @@ -1069,6 +1200,8 @@ export function recoverLegacyRestore({ targetRoot, confirmOffline = false } = {} const root = path.resolve(targetRoot); const parent = path.dirname(root); + assertNoWindowsCaseAliasedRestoreJournal(root); + assertNoWindowsOrphanRestoreStages(root); const legacyJournal = legacyRestoreJournalPath(root); if (fs.existsSync(legacyJournal)) { return recoverLegacyRestoreV1({ root, parent, journalPath: legacyJournal }); @@ -1082,15 +1215,18 @@ export function recoverLegacyRestore({ targetRoot, confirmOffline = false } = {} const latest = latestRestoreTransaction(records); const tx = latest.prepared; - const expectedStage = '.dk-legacy-restore-stage-' + tx.transactionId; - if (tx.targetRoot !== root || tx.stageName !== expectedStage) { + const legacyStage = '.dk-legacy-restore-stage-' + tx.transactionId; + const scopedStage = '.dk-legacy-restore-stage-' + restoreJournalDigest(root) + '-' + tx.transactionId; + if (tx.targetRoot !== root || ![legacyStage, scopedStage].includes(tx.stageName)) { throw new StateMigrationError('Restore transaction witness does not match requested target root'); } - const stage = path.join(parent, expectedStage); + const stage = path.join(parent, tx.stageName); const displaced = stage + '-original'; if (latest.last.phase === RESTORE_TERMINAL_PHASE) { + // Never treat an authenticated terminal record as physical root evidence. + requireTerminalRestoreRootWitness(root, tx, latest.last.outcome); return Object.freeze({ recovered: false, alreadyComplete: true, @@ -1140,19 +1276,21 @@ export function recoverLegacyRestore({ targetRoot, confirmOffline = false } = {} if (sameRestoreIdentity(atRoot, tx.stageDev, tx.stageIno) && ((!tx.existed && !inDisplaced) || (tx.existed && inDisplaced))) { + // The candidate may be visible after a successful rename whose parent + // directory flush failed or never ran before process death. Retry the + // publication namespace barrier *before* appending acknowledgement or + // terminal records. On failure, preserve the nonterminal journal and both + // candidate/original witnesses for another controlled recovery attempt. + syncRestoreParent(parent); appendMissingSuccessfulRestorePhases(root, tx, latest.records, { outcome: tx.existed ? 'promoted-original-retained' : 'promoted-new-root', }); - let cleanupPending = false; - if (tx.existed && fs.existsSync(displaced)) { - try { - fs.rmSync(displaced, { recursive: true, force: true }); - syncRestoreParent(parent); - } catch { - cleanupPending = true; - } - } + // The old root is the only exact-inode rollback witness after promotion. + // A terminal journal record cannot certify physical persistence of the + // preceding Windows namespace operations. Retain it for separate offline + // reviewed cleanup; never delete it as part of automatic recovery. + let cleanupPending = tx.existed && fs.existsSync(displaced); if (inStage && fs.existsSync(stage)) { try { fs.rmSync(stage, { recursive: true, force: true }); @@ -1280,6 +1418,9 @@ export function restoreLegacyBackup({ phase: latest.last.phase, }); } + // A second restore may not overwrite or supersede a terminal journal + // whose prior root publication was lost across power interruption. + requireTerminalRestoreRootWitness(resolvedRoot, latest.prepared, latest.last.outcome); } const bundle = readJson(backupPath, 'Legacy backup'); @@ -1324,7 +1465,8 @@ export function restoreLegacyBackup({ syncRestoreParent(parent); const transactionId = crypto.randomBytes(16).toString('hex'); - const stage = path.join(parent, '.dk-legacy-restore-stage-' + transactionId); + const stage = path.join(parent, '.dk-legacy-restore-stage-' + + restoreJournalDigest(resolvedRoot) + '-' + transactionId); const displaced = stage + '-original'; fs.mkdirSync(stage, { mode: 0o700 }); @@ -1429,17 +1571,11 @@ export function restoreLegacyBackup({ outcome: originalRootExists ? 'promoted-original-retained' : 'promoted-new-root', }); - let cleanupPending = false; - if (originalRootExists && fs.existsSync(displaced)) { - try { - fs.rmSync(displaced, { recursive: true, force: true }); - syncRestoreParent(parent); - } catch { - // Cleanup is intentionally post-terminal. Preserve the authenticated - // old root rather than turning cleanup failure into data loss. - cleanupPending = true; - } - } + // A completed restore retains the displaced original as an exact-inode + // recovery witness. Its later disposal is a distinct offline operation, + // requiring explicit retention/security approval and disk-space planning. + // Do not destroy the original merely because the terminal log was fsynced. + const cleanupPending = originalRootExists && fs.existsSync(displaced); return Object.freeze({ restoredFiles: restored.sort(), diff --git a/scripts/autopilot.test.mjs b/scripts/autopilot.test.mjs index 22f382f6e..b469d7fb8 100644 --- a/scripts/autopilot.test.mjs +++ b/scripts/autopilot.test.mjs @@ -92,6 +92,12 @@ test('3. Immutable Revision Persistence & Reading', () => { test('4. Corrupt Pointer Recovery', () => { const tmpDir = createTempDir(); const state1 = createInitialState({ autonomy: 'guided-autopilot' }, tmpDir); + // This test intentionally corrupts the legacy current.json pointer. Seed + // an unmigrated legacy revision so the v0.13 compatibility adapter keeps + // using the legacy authority rather than a fresh canonical V2 ledger. + const legacyDir = path.join(tmpDir, '.development-kit', 'autopilot', 'state'); + fs.mkdirSync(legacyDir, { recursive: true }); + fs.writeFileSync(path.join(legacyDir, 'revision-000001.json'), JSON.stringify(state1, null, 2)); saveStateRevision(state1, tmpDir); // Corrupt current.json diff --git a/scripts/lifecycle-instances.test.mjs b/scripts/lifecycle-instances.test.mjs index 53090c26a..9a3469208 100644 --- a/scripts/lifecycle-instances.test.mjs +++ b/scripts/lifecycle-instances.test.mjs @@ -232,6 +232,12 @@ test('AC-016: implicit legacy lifecycle rehydrates stable persisted snapshot wit state.currentStage = 'REVIEW'; state.completedStages = ['UNDERSTAND', 'DEFINE', 'DESIGN', 'PLAN', 'IMPLEMENT', 'VERIFY']; state.stateRevision = 3; + // This fixture specifically exercises an existing, unmigrated legacy state + // authority. Fresh v0.13 projects intentionally use the canonical ledger; + // seed an actual legacy revision before invoking the compatibility adapter. + const legacyStateDir = path.join(rootDir, '.development-kit', 'autopilot', 'state'); + fs.mkdirSync(legacyStateDir, { recursive: true }); + fs.writeFileSync(path.join(legacyStateDir, 'revision-000003.json'), JSON.stringify(state, null, 2)); saveStateRevision(state, rootDir); const pointer = path.join(rootDir, '.development-kit', 'autopilot', 'state', 'current.json'); const previous = fs.readFileSync(pointer); diff --git a/scripts/phase01-scale-context-baseline.mjs b/scripts/phase01-scale-context-baseline.mjs index bbba912e1..00d3d26e2 100644 --- a/scripts/phase01-scale-context-baseline.mjs +++ b/scripts/phase01-scale-context-baseline.mjs @@ -140,12 +140,20 @@ function legacyStateFixture(revisions = 26) { const root = tempProject('legacy-state'); try { const state = createInitialState({ autonomy: 'guided-autopilot' }, root); + // This is a *historical legacy micro-file* baseline, not a measurement of + // the v0.13 canonical ledger. Explicitly seed a valid legacy revision so + // the compatibility adapter exercises the old persistence authority. + // The first save overwrites this seed and writes the normal current.json + // pointer; the measured file count remains exactly revisions + 1. + const stateDir = getStateDir(root); + fs.mkdirSync(stateDir, { recursive: true }); + fs.writeFileSync(path.join(stateDir, 'revision-000001.json'), + JSON.stringify({ ...state, stateRevision: 1 }, null, 2)); for (let revision = 1; revision <= revisions; revision += 1) { state.stateRevision = revision; state.updatedAt = new Date(Date.parse(state.createdAt) + revision * 1000).toISOString(); saveStateRevision(state, root); } - const stateDir = getStateDir(root); const files = fs.readdirSync(stateDir).filter((name) => fs.statSync(path.join(stateDir, name)).isFile()); const revisionFiles = files.filter((name) => /^revision-\d{6}\.json$/.test(name)); const bytes = files.reduce((sum, name) => sum + fs.statSync(path.join(stateDir, name)).size, 0); diff --git a/scripts/state-engine-v013.test.mjs b/scripts/state-engine-v013.test.mjs index f3e01c820..bbd4fb946 100644 --- a/scripts/state-engine-v013.test.mjs +++ b/scripts/state-engine-v013.test.mjs @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; +import crypto from 'node:crypto'; import os from 'node:os'; import path from 'node:path'; @@ -8,6 +9,7 @@ import { appendMetadataEvent, getStateEnginePaths, loadCanonicalEvents, + loadEntityState, loadStateSnapshot, rebuildStateIndex, verifyStateEngineIntegrity, @@ -20,6 +22,7 @@ import { } from '../runtime/orchestration/state-engine-migration.mjs'; import { createInitialState } from '../runtime/autopilot/transition-model.mjs'; import { saveStateRevision } from '../runtime/autopilot/state-store.mjs'; +import { persistRunManifest, persistRunStateRevision, updateRun } from '../runtime/orchestration/orchestration-run.mjs'; function tempDir(t, prefix = 'dk-v013-state-') { const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); @@ -221,3 +224,298 @@ test('W73-10 no transaction and no witness reports no recovery work', (t) => { { recovered: false }, ); }); + + +test('W73-11 terminal restore log cannot certify a missing promoted root', (t) => { + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-terminal-witness-'); + const targetRoot = path.join(parent, 'restored-project'); + const result = restoreLegacyBackup({ backupPath, targetRoot, overwrite: true, confirmOffline: true }); + assert.equal(fs.existsSync(result.transactionLog), true); + assert.equal(fs.existsSync(targetRoot), true); + + // Model a persisted terminal record whose corresponding directory rename + // was not durable across a power interruption. No attacker is assumed. + // A terminal record must never be sufficient to declare recovery complete. + const moved = path.join(parent, 'simulated-unpersisted-promotion'); + fs.renameSync(targetRoot, moved); + assert.throws( + () => recoverLegacyRestore({ targetRoot, confirmOffline: true }), + /terminal.*(root|witness)|terminal.*identity/i, + 'Missing promoted root must fail closed despite a valid terminal journal', + ); + assert.equal(fs.existsSync(moved), true, 'Do not destroy recovery evidence'); + assert.equal(fs.existsSync(result.transactionLog), true, 'Retain authenticated journal'); +}); + + +test('W73-12 completed Windows-compatible restore retains exact original inode after terminal acknowledgement', (t) => { + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-retained-original-'); + const targetRoot = path.join(parent, 'project'); + fs.mkdirSync(targetRoot); + const originalSentinel = path.join(targetRoot, 'original-witness.txt'); + fs.writeFileSync(originalSentinel, 'ORIGINAL-MUST-SURVIVE'); + const before = fs.lstatSync(targetRoot, { bigint: true }); + + const restored = restoreLegacyBackup({ + backupPath, targetRoot, overwrite: true, confirmOffline: true, + }); + assert.ok(restored.displacedOriginal, + 'The original directory must remain an independently verifiable rollback witness'); + const retained = fs.lstatSync(restored.displacedOriginal, { bigint: true }); + assert.equal(retained.dev, before.dev); + assert.equal(retained.ino, before.ino, 'Original directory inode must be retained'); + assert.equal(fs.readFileSync(path.join(restored.displacedOriginal, 'original-witness.txt'), 'utf8'), + 'ORIGINAL-MUST-SURVIVE'); + assert.equal(fs.existsSync(restored.transactionLog), true, 'Retain terminal transaction log'); + const recovered = recoverLegacyRestore({ targetRoot, confirmOffline: true }); + assert.equal(recovered.alreadyComplete, true); + assert.equal(fs.existsSync(restored.displacedOriginal), true, + 'Read-only completed recovery must not delete the original witness'); +}); + + +for (const linkKind of ['symbolic', 'hard']) { + test(`W73-13/${linkKind} pre-existing linked restore journal cannot redirect transaction writes`, (t) => { + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-journal-link-'); + const targetRoot = path.join(parent, 'project'); + const outside = path.join(parent, 'unrelated-confidential-log.jsonl'); + fs.writeFileSync(outside, ''); + const digest = crypto.createHash('sha256').update(path.resolve(targetRoot)).digest('hex').slice(0, 24); + const journal = path.join(parent, '.dk-restore-transaction-' + digest + '.jsonl'); + try { + if (linkKind === 'symbolic') fs.symlinkSync(outside, journal, 'file'); + else fs.linkSync(outside, journal); + } catch (error) { + if (['EPERM', 'EACCES', 'ENOTSUP', 'EOPNOTSUPP'].includes(error.code)) { + t.skip('Host does not permit this linked-file fixture'); return; + } + throw error; + } + assert.throws( + () => restoreLegacyBackup({ backupPath, targetRoot, confirmOffline: true }), + /restore.*(journal|transaction|link)|unsafe.*(link|journal)/i, + 'Existing linked journal must fail closed before writing external bytes', + ); + assert.equal(fs.readFileSync(outside, 'utf8'), '', 'Unrelated target must remain untouched'); + assert.equal(fs.existsSync(targetRoot), false); + }); +} + + +test('W73-14 retained original from one project must not block unrelated target in same parent', (t) => { + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-independent-targets-'); + const firstRoot = path.join(parent, 'first'); + const secondRoot = path.join(parent, 'second'); + fs.mkdirSync(firstRoot); + fs.writeFileSync(path.join(firstRoot, 'first-original.txt'), 'FIRST-ORIGINAL'); + const first = restoreLegacyBackup({ backupPath, targetRoot: firstRoot, overwrite: true, confirmOffline: true }); + assert.ok(first.displacedOriginal); + assert.equal(fs.existsSync(first.displacedOriginal), true); + + const second = restoreLegacyBackup({ backupPath, targetRoot: secondRoot, confirmOffline: true }); + assert.equal(fs.existsSync(secondRoot), true, + 'Unrelated target must remain independently restorable'); + assert.equal(fs.existsSync(second.transactionLog), true); + assert.equal(fs.readFileSync(path.join(first.displacedOriginal, 'first-original.txt'), 'utf8'), + 'FIRST-ORIGINAL', 'Original rollback witness must survive the other restore'); +}); + + +test('W73-15 a terminal journal with a missing root blocks the next restore without superseding evidence', (t) => { + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-terminal-preflight-'); + const targetRoot = path.join(parent, 'project'); + fs.mkdirSync(targetRoot); + fs.writeFileSync(path.join(targetRoot, 'original.txt'), 'KEEP-ORIGINAL'); + const first = restoreLegacyBackup({ backupPath, targetRoot, overwrite: true, confirmOffline: true }); + assert.ok(first.displacedOriginal); + const before = fs.readFileSync(first.transactionLog, 'utf8'); + + // Model terminal journal persistence without the preceding root publication. + const unpersisted = path.join(parent, 'unpersisted-published-root'); + fs.renameSync(targetRoot, unpersisted); + assert.throws( + () => restoreLegacyBackup({ backupPath, targetRoot, confirmOffline: true }), + /terminal.*(root|witness)|terminal.*identity/i, + 'A newer restore must not supersede an unresolved terminal root witness', + ); + assert.equal(fs.existsSync(targetRoot), false); + assert.equal(fs.readFileSync(first.transactionLog, 'utf8'), before); + assert.equal(fs.readFileSync(path.join(first.displacedOriginal, 'original.txt'), 'utf8'), 'KEEP-ORIGINAL'); +}); + +test('W73-16 imported V1 terminal outcomes remain idempotently recoverable', (t) => { + const parent = tempDir(t, 'dk-v013-imported-v1-'); + const targetRoot = path.join(parent, 'project'); + fs.mkdirSync(targetRoot); + fs.writeFileSync(path.join(targetRoot, 'original.txt'), 'V1-ORIGINAL'); + const original = fs.lstatSync(targetRoot, { bigint: true }); + const transactionId = 'a'.repeat(32); + const stageName = '.dk-legacy-restore-stage-' + transactionId; + const digest = crypto.createHash('sha256').update(path.resolve(targetRoot)).digest('hex').slice(0, 24); + const journal = path.join(parent, '.dk-restore-journal-' + digest + '.json'); + fs.writeFileSync(journal, JSON.stringify({ + schemaVersion: '1.0.0', transactionId, targetRoot, stageName, + existed: true, originalDev: original.dev.toString(), originalIno: original.ino.toString(), + stageDev: '1', stageIno: '1', + })); + const first = recoverLegacyRestore({ targetRoot, confirmOffline: true }); + assert.equal(first.legacyJournal, true); + assert.equal(first.outcome, 'uncommitted-original-unchanged'); + assert.equal(fs.existsSync(first.retainedJournal), true); + const second = recoverLegacyRestore({ targetRoot, confirmOffline: true }); + assert.equal(second.alreadyComplete, true); + assert.equal(fs.readFileSync(path.join(targetRoot, 'original.txt'), 'utf8'), 'V1-ORIGINAL'); +}); + +test('W73-17 mixed legacy Autopilot and canonical V2 orchestration runs migrate without revision regression', (t) => { + const rootDir = tempDir(t, 'dk-v013-mixed-state-'); + const legacy = createInitialState({ autonomy: 'guided-autopilot' }, rootDir); + const legacyDir = path.join(rootDir, '.development-kit', 'autopilot', 'state'); + fs.mkdirSync(legacyDir, { recursive: true }); + fs.writeFileSync(path.join(legacyDir, 'revision-000001.json'), JSON.stringify(legacy, null, 2)); + saveStateRevision(legacy, rootDir); + + const run = { + schemaVersion: '1.0.0', contractId: 'contract-mixed', taskId: 'task-mixed', + runId: 'run-mixed', sourceFingerprint: 'sha256:' + 'a'.repeat(64), + createdAt: '2026-10-30T00:00:00.000Z', updatedAt: '2026-10-30T00:00:00.000Z', + stateRevision: 1, state: 'READY', executionStrategy: 'direct', + hostCapabilities: {}, manualEvidenceRequired: false, requiredGates: {}, + completedGates: [], correctionAttempt: 0, failureSignatures: [], + verificationVerdict: null, acceptanceState: 'PENDING', + }; + persistRunManifest(run, rootDir); + assert.equal(persistRunStateRevision(run, rootDir).stateEngine, 'v2'); + const revised = updateRun(run, { state: 'IMPLEMENTING' }, '2026-10-30T00:01:00.000Z'); + assert.equal(persistRunStateRevision(revised, rootDir).stateEngine, 'v2'); + const migration = migrateLegacyStateToV2({ rootDir, completedAt: '2026-10-30T00:02:00.000Z' }); + assert.equal(migration.semanticEquivalence, true); + assert.equal(loadEntityState('orchestration-run', 'contract-mixed/run-mixed', rootDir).stateRevision, 2); +}); + + +test('W73-18 recovery cannot acknowledge candidate promotion without retrying parent flush', (t) => { + if (process.platform === 'win32') { + t.skip('POSIX fsync injection; Windows native-helper barrier requires its own fault hook'); return; + } + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-publication-barrier-'); + const targetRoot = path.join(parent, 'project'); + fs.mkdirSync(targetRoot); + fs.writeFileSync(path.join(targetRoot, 'original.txt'), 'RETAIN-ORIGINAL'); + const originalRename = fs.renameSync; + const originalFsync = fs.fsyncSync; + const parentStat = fs.statSync(parent); + let promoted = false; + let injected = false; + fs.renameSync = function watchPromotion(from, to, ...args) { + const result = originalRename.call(this, from, to, ...args); + if (typeof from === 'string' && from.includes('.dk-legacy-restore-stage-') && + !from.endsWith('-original') && to === targetRoot) promoted = true; + return result; + }; + fs.fsyncSync = function failPublicationBarrier(fd) { + const stat = fs.fstatSync(fd); + if (promoted && !injected && stat.isDirectory() && + stat.dev === parentStat.dev && stat.ino === parentStat.ino) { + injected = true; + throw new Error('simulated publication parent flush failure'); + } + return originalFsync.call(this, fd); + }; + try { + assert.throws( + () => restoreLegacyBackup({ backupPath, targetRoot, overwrite: true, confirmOffline: true }), + /simulated publication parent flush failure|flush barrier failed/i, + ); + assert.equal(injected, true); + } finally { + fs.renameSync = originalRename; + fs.fsyncSync = originalFsync; + } + const journal = fs.readdirSync(parent).find((name) => name.startsWith('.dk-restore-transaction-')); + assert.ok(journal); + const journalPath = path.join(parent, journal); + assert.equal(fs.readFileSync(journalPath, 'utf8').includes('"phase":"CLEANUP_SAFE"'), false); + + let recoveryBarrierAttempted = false; + fs.fsyncSync = function failRecoveryBarrier(fd) { + const stat = fs.fstatSync(fd); + if (!recoveryBarrierAttempted && stat.isDirectory() && + stat.dev === parentStat.dev && stat.ino === parentStat.ino) { + recoveryBarrierAttempted = true; + throw new Error('simulated recovery publication flush failure'); + } + return originalFsync.call(this, fd); + }; + try { + assert.throws( + () => recoverLegacyRestore({ targetRoot, confirmOffline: true }), + /simulated recovery publication flush failure|flush barrier failed/i, + ); + assert.equal(recoveryBarrierAttempted, true, 'Recovery must retry the publication flush'); + assert.equal(fs.readFileSync(journalPath, 'utf8').includes('"phase":"CLEANUP_SAFE"'), false, + 'No terminal acknowledgement after a failed recovery barrier'); + } finally { + fs.fsyncSync = originalFsync; + } + const recovered = recoverLegacyRestore({ targetRoot, confirmOffline: true }); + assert.equal(recovered.outcome, 'promoted-original-retained'); + assert.equal(fs.existsSync(recovered.displacedOriginal), true); +}); + + +test('W73-19 Windows case alias cannot bypass the existing restore transaction witness', (t) => { + if (process.platform !== 'win32') { + t.skip('NTFS case-insensitive alias behavior requires Windows'); return; + } + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-ntfs-case-alias-'); + const targetRoot = path.join(parent, 'MixedCaseProject'); + const aliasRoot = path.join(parent, 'mixedcaseproject'); + fs.mkdirSync(targetRoot); + fs.writeFileSync(path.join(targetRoot, 'original.txt'), 'ORIGINAL-CASE-ALIAS'); + const first = restoreLegacyBackup({ backupPath, targetRoot, overwrite: true, confirmOffline: true }); + assert.ok(first.displacedOriginal); + const journalBefore = fs.readFileSync(first.transactionLog, 'utf8'); + const moved = path.join(parent, 'unpersisted-published-root'); + fs.renameSync(targetRoot, moved); + assert.throws( + () => restoreLegacyBackup({ backupPath, targetRoot: aliasRoot, confirmOffline: true }), + /terminal.*(root|witness)|incomplete restore|restore witnesses/i, + 'Case alias must not start a new transaction over an unresolved original', + ); + assert.equal(fs.readFileSync(first.transactionLog, 'utf8'), journalBefore); + assert.equal(fs.existsSync(aliasRoot), false); + assert.equal(fs.readFileSync(path.join(first.displacedOriginal, 'original.txt'), 'utf8'), + 'ORIGINAL-CASE-ALIAS'); +}); + + +test('W73-20 Windows case alias cannot bypass an orphan displaced original after journal loss', (t) => { + if (process.platform !== 'win32') { + t.skip('NTFS case-insensitive alias behavior requires Windows'); return; + } + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-orphan-case-alias-'); + const targetRoot = path.join(parent, 'OriginalCaseProject'); + const aliasRoot = path.join(parent, 'originalcaseproject'); + fs.mkdirSync(targetRoot); + fs.writeFileSync(path.join(targetRoot, 'original.txt'), 'ORIGINAL-ORPHAN'); + const first = restoreLegacyBackup({ backupPath, targetRoot, overwrite: true, confirmOffline: true }); + const moved = path.join(parent, 'unpersisted-published-root'); + fs.renameSync(targetRoot, moved); + fs.unlinkSync(first.transactionLog); // Simulate journal namespace loss after an interrupted publication. + assert.throws( + () => restoreLegacyBackup({ backupPath, targetRoot: aliasRoot, confirmOffline: true }), + /restore witnesses|restore.*journal|orphan.*restore|recovery journal/i, + 'An orphan retained original must not be silently superseded via case alias', + ); + assert.equal(fs.existsSync(aliasRoot), false); + assert.equal(fs.readFileSync(path.join(first.displacedOriginal, 'original.txt'), 'utf8'), 'ORIGINAL-ORPHAN'); +}); diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index 251cf55e1..a15e9ed9b 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -499,6 +499,10 @@ test('T04-H29 interrupted ledger recovery must never truncate an external hardli test('T04-H36 pending journal cannot overwrite external file after state-directory swap', (t) => { + if (process.platform === 'win32') { + t.skip('Pinned Windows directory handles prevent this POSIX directory-swap fixture'); + return; + } const rootDir = tempProject(t); appendMetadataEvent({ rootDir, eventType: 'FIRST', payload: { value: 1 } }); const paths = getStateEnginePaths(rootDir); @@ -551,6 +555,10 @@ test('T04-H36 pending journal cannot overwrite external file after state-directo }); test('T04-H37 pending journal unlink fails closed when state directory is replaced during commit', (t) => { + if (process.platform === 'win32') { + t.skip('Pinned Windows directory handles prevent this POSIX directory-swap fixture'); + return; + } const rootDir = tempProject(t); appendMetadataEvent({ rootDir, eventType: 'FIRST', payload: { value: 1 } }); const paths = getStateEnginePaths(rootDir); @@ -611,6 +619,10 @@ test('T04-H37 pending journal unlink fails closed when state directory is replac }); test('T04-H38 pending journal unlink fails closed when state directory is swapped with an ordinary directory across transaction', (t) => { + if (process.platform === 'win32') { + t.skip('Pinned Windows directory handles prevent this POSIX directory-swap fixture'); + return; + } const rootDir = tempProject(t); appendMetadataEvent({ rootDir, eventType: 'FIRST', payload: { value: 1 } }); const paths = getStateEnginePaths(rootDir); @@ -659,6 +671,10 @@ test('T04-H38 pending journal unlink fails closed when state directory is swappe }); test('T04-H40 durable transaction writes fail closed when state directory is replaced before append or derived writes', (t) => { + if (process.platform === 'win32') { + t.skip('Pinned Windows directory handles prevent this POSIX directory-swap fixture'); + return; + } const rootDir = tempProject(t); appendMetadataEvent({ rootDir, eventType: 'FIRST', payload: { value: 1 } }); const paths = getStateEnginePaths(rootDir); diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index fc624367c..a9efed55e 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -665,22 +665,41 @@ test('T04-H25 interrupted two-step root promotion can restore original using jou const sentinel = path.join(root, 'persist-original.txt'); fs.writeFileSync(sentinel, 'ORIGINAL'); const originalRename = fs.renameSync; + const originalWrite = fs.writeFileSync; let movedRoot = false; let failureInjected = false; - fs.renameSync = function injectPowerFailure(oldPath, nextPath, ...args) { - if (movedRoot && !failureInjected && typeof oldPath === 'string' && - oldPath.includes('.dk-legacy-restore-stage-') && nextPath === root) { - failureInjected = true; - throw new Error('simulated crash between displacement and promotion'); - } - if (oldPath === root && String(nextPath).endsWith('-original')) movedRoot = true; - return originalRename.call(this, oldPath, nextPath, ...args); - }; + if (process.platform === 'win32') { + // Windows uses the native helper for renames. Inject immediately after + // the durable ORIGINAL_DISPLACED log write, before stage promotion. + fs.writeFileSync = function injectWindowsRestoreFailure(fd, content, ...args) { + const result = originalWrite.call(this, fd, content, ...args); + if (!failureInjected && typeof fd === 'number' && + String(content).includes('"phase":"ORIGINAL_DISPLACED"')) { + failureInjected = true; + throw new Error('simulated crash between displacement and promotion'); + } + return result; + }; + } else { + fs.renameSync = function injectPowerFailure(oldPath, nextPath, ...args) { + if (movedRoot && !failureInjected && typeof oldPath === 'string' && + oldPath.includes('.dk-legacy-restore-stage-') && nextPath === root) { + failureInjected = true; + throw new Error('simulated crash between displacement and promotion'); + } + if (oldPath === root && String(nextPath).endsWith('-original')) movedRoot = true; + return originalRename.call(this, oldPath, nextPath, ...args); + }; + } try { assert.throws(() => restoreLegacyBackup({ confirmOffline: true, backupPath: migrated.backupPath, targetRoot: root, }), /simulated crash/); - } finally { fs.renameSync = originalRename; } + assert.equal(failureInjected, true, 'The intended crash boundary must actually be reached'); + } finally { + fs.renameSync = originalRename; + fs.writeFileSync = originalWrite; + } // If the in-process finally managed the rollback, the leftover journal is // still authoritative. Recovery must be idempotent and must never erase it. assert.equal(fs.readFileSync(sentinel, 'utf8'), 'ORIGINAL'); @@ -711,11 +730,19 @@ test('T04-H28 restore journals preserve original after a simulated process death const script = [ 'import fs from "node:fs";', 'import {restoreLegacyBackup} from '+JSON.stringify(new URL('../runtime/orchestration/state-engine-migration.mjs', import.meta.url).href)+';', - 'const backup=process.argv[1],root=process.argv[2],rename=fs.renameSync;', - 'fs.renameSync=function(a,b,...rest){', - ' if(a===root && String(b).endsWith("-original")) {const r=rename.call(this,a,b,...rest);process.exit(91);}', - ' return rename.call(this,a,b,...rest);', - '};', + 'const backup=process.argv[1],root=process.argv[2],rename=fs.renameSync,write=fs.writeFileSync;', + 'if(process.platform==="win32") {', + ' fs.writeFileSync=function(fd,content,...rest){', + ' const out=write.call(this,fd,content,...rest);', + ' if(typeof fd==="number" && String(content).includes(\'"phase":"ORIGINAL_DISPLACED"\')) process.exit(91);', + ' return out;', + ' };', + '} else {', + ' fs.renameSync=function(a,b,...rest){', + ' if(a===root && String(b).endsWith("-original")) {const out=rename.call(this,a,b,...rest);process.exit(91);}', + ' return rename.call(this,a,b,...rest);', + ' };', + '}', 'restoreLegacyBackup({backupPath:backup,targetRoot:root,confirmOffline:true});', ].join('\n'); // spawnSync is intentionally used for a true process exit instead of a @@ -733,8 +760,10 @@ test('T04-H28 restore journals preserve original after a simulated process death }); -test('T04-H47 recovery fsyncs the restored parent entry before deleting its rollback journal', (t) => { - // Runs on Windows too: the barrier is FlushFileBuffers on a directory handle. +test('T04-H47 POSIX rollback parent flush precedes append-only terminal acknowledgement', (t) => { + if (process.platform === 'win32') { + t.skip('Native Windows rename/flush is exercised by H48'); return; + } const source = tempProject(t, 'dk-rollback-fsync-source-'); writeLegacyAutopilot(source, 2); const migrated = migrateLegacyStateToV2({ rootDir: source }); @@ -742,7 +771,6 @@ test('T04-H47 recovery fsyncs the restored parent entry before deleting its roll const sentinel = path.join(root, 'original.txt'); fs.writeFileSync(sentinel, 'ROLLBACK-ORIGINAL', 'utf8'); - // A real process exit avoids running the normal in-process finally rollback. const script = [ 'import fs from "node:fs";', 'import {restoreLegacyBackup} from ' + @@ -763,53 +791,48 @@ test('T04-H47 recovery fsyncs the restored parent entry before deleting its roll const parentStat = fs.statSync(parent); const originalRename = fs.renameSync; const originalFsync = fs.fsyncSync; - const originalUnlink = fs.unlinkSync; + const originalWrite = fs.writeFileSync; let rollbackRenamed = false; let parentSyncedAfterRollback = false; - let journalRemoved = false; + let terminalRecorded = false; fs.renameSync = function watchRollback(from, to, ...args) { const returned = originalRename.call(this, from, to, ...args); - if (typeof from === 'string' && from.endsWith('-original') && to === root) { - rollbackRenamed = true; - } + if (typeof from === 'string' && from.endsWith('-original') && to === root) rollbackRenamed = true; return returned; }; fs.fsyncSync = function watchParentDurability(fd) { const stat = fs.fstatSync(fd); + const returned = originalFsync.call(this, fd); if (rollbackRenamed && stat.isDirectory() && - stat.dev === parentStat.dev && stat.ino === parentStat.ino) { - parentSyncedAfterRollback = true; - } - return originalFsync.call(this, fd); + stat.dev === parentStat.dev && stat.ino === parentStat.ino) parentSyncedAfterRollback = true; + return returned; }; - fs.unlinkSync = function guardJournalRemoval(target, ...args) { - if (typeof target === 'string' && - path.basename(target).startsWith('.dk-restore-journal-')) { - journalRemoved = true; + fs.writeFileSync = function guardTerminalRecord(fd, content, ...args) { + if (typeof fd === 'number' && String(content).includes('"phase":"CLEANUP_SAFE"')) { + terminalRecorded = true; assert.equal(rollbackRenamed, true, 'Recovery must restore the displaced root'); assert.equal(parentSyncedAfterRollback, true, - 'Rollback directory rename must be fsynced before deleting the journal'); + 'Rollback directory rename must be fsynced before durable terminal log append'); } - return originalUnlink.call(this, target, ...args); + return originalWrite.call(this, fd, content, ...args); }; try { const recovery = recoverLegacyRestore({ targetRoot: root, confirmOffline: true }); assert.equal(recovery.outcome, 'rolled-back-original'); - assert.ok(journalRemoved); + assert.ok(terminalRecorded, 'Append-only terminal log must acknowledge rollback'); + assert.equal(fs.existsSync(recovery.transactionLog), true, 'Retain the hash-chained recovery log'); assert.equal(fs.readFileSync(sentinel, 'utf8'), 'ROLLBACK-ORIGINAL'); } finally { fs.renameSync = originalRename; fs.fsyncSync = originalFsync; - fs.unlinkSync = originalUnlink; + fs.writeFileSync = originalWrite; } }); -test('T04-H48 Windows crash rollback and recovery verify parent flush barrier before journal unlink', (t) => { +test('T04-H48 Windows native rollback completes directory flush before retained terminal log', (t) => { if (process.platform !== 'win32') { - t.skip('Windows parent flush barrier verification'); - return; + t.skip('Windows native durability helper requires Windows NTFS'); return; } - const source = tempProject(t, 'dk-win-rollback-source-'); writeLegacyAutopilot(source, 2); const migrated = migrateLegacyStateToV2({ rootDir: source }); @@ -817,14 +840,17 @@ test('T04-H48 Windows crash rollback and recovery verify parent flush barrier be const sentinel = path.join(root, 'original.txt'); fs.writeFileSync(sentinel, 'WINDOWS-ORIGINAL', 'utf8'); + // The v0.13 Windows path uses an isolated native helper, not fs.renameSync. + // Kill the child only after the ORIGINAL_DISPLACED record has been written. const script = [ 'import fs from "node:fs";', 'import {restoreLegacyBackup} from ' + JSON.stringify(new URL('../runtime/orchestration/state-engine-migration.mjs', import.meta.url).href) + ';', - 'const backup=process.argv[1],root=process.argv[2],rename=fs.renameSync;', - 'fs.renameSync=function(a,b,...rest){', - ' if(a===root && String(b).endsWith("-original")) {rename.call(this,a,b,...rest);process.exit(91);}', - ' return rename.call(this,a,b,...rest);', + 'const backup=process.argv[1],root=process.argv[2],write=fs.writeFileSync;', + 'fs.writeFileSync=function(fd,content,...rest){', + ' const out=write.call(this,fd,content,...rest);', + ' if(typeof fd==="number" && String(content).includes(\'"phase":"ORIGINAL_DISPLACED"\')) process.exit(91);', + ' return out;', '};', 'restoreLegacyBackup({backupPath:backup,targetRoot:root,confirmOffline:true});', ].join('\n'); @@ -833,51 +859,13 @@ test('T04-H48 Windows crash rollback and recovery verify parent flush barrier be assert.equal(result.status, 91, result.stderr); assert.equal(fs.existsSync(root), false); - const parent = path.dirname(root); - const parentIdentity = fs.statSync(parent); - const originalRename = fs.renameSync; - const originalFsync = fs.fsyncSync; - const originalUnlink = fs.unlinkSync; - let rollbackRenamed = false; - let parentFlushedAfterRollback = false; - let journalRemoved = false; - - fs.renameSync = function watchRollback(from, to, ...args) { - const returned = originalRename.call(this, from, to, ...args); - if (typeof from === 'string' && from.endsWith('-original') && to === root) { - rollbackRenamed = true; - } - return returned; - }; - fs.fsyncSync = function watchParentFlush(fd) { - const stat = fs.fstatSync(fd); - const returned = originalFsync.call(this, fd); - if (rollbackRenamed && stat.isDirectory() && - stat.dev === parentIdentity.dev && stat.ino === parentIdentity.ino) { - parentFlushedAfterRollback = true; - } - return returned; - }; - fs.unlinkSync = function guardJournalRemoval(target, ...args) { - if (typeof target === 'string' && - path.basename(target).startsWith('.dk-restore-journal-')) { - journalRemoved = true; - assert.equal(rollbackRenamed, true, 'Recovery must restore the displaced root'); - assert.equal(parentFlushedAfterRollback, true, - 'Windows rollback directory rename must be flushed to disk before deleting the journal'); - } - return originalUnlink.call(this, target, ...args); - }; - try { - const recovered = recoverLegacyRestore({ targetRoot: root, confirmOffline: true }); - assert.equal(recovered.outcome, 'rolled-back-original'); - assert.ok(journalRemoved); - assert.equal(fs.readFileSync(sentinel, 'utf8'), 'WINDOWS-ORIGINAL'); - } finally { - fs.renameSync = originalRename; - fs.fsyncSync = originalFsync; - fs.unlinkSync = originalUnlink; - } + const recovery = recoverLegacyRestore({ targetRoot: root, confirmOffline: true }); + assert.equal(recovery.outcome, 'rolled-back-original'); + assert.equal(fs.readFileSync(sentinel, 'utf8'), 'WINDOWS-ORIGINAL'); + assert.equal(fs.existsSync(recovery.transactionLog), true, 'Native recovery retains journal'); + const records = fs.readFileSync(recovery.transactionLog, 'utf8').trimEnd().split('\n').map(JSON.parse); + assert.equal(records.at(-1).phase, 'CLEANUP_SAFE'); + assert.equal(records.at(-1).outcome, 'rolled-back-original'); }); test('T04-H30 offline staged restore preserves descendant file identity and POSIX permission metadata', (t) => { diff --git a/scripts/t04-review-thread-blockers.mjs b/scripts/t04-review-thread-blockers.mjs index cb788183d..001b303cf 100644 --- a/scripts/t04-review-thread-blockers.mjs +++ b/scripts/t04-review-thread-blockers.mjs @@ -98,9 +98,9 @@ function requireCanonicalRole(body, label, expectedRole) { } export function requireCanonicalGithubReviewRequest(request, expectedRole, reviewedCommit) { - if (!request || request.issue_url !== 'https://api.github.com/repos/eybersjp/development-kit/issues/70' || + if (!request || request.issue_url !== 'https://api.github.com/repos/eybersjp/development-kit/issues/92' || typeof request.body !== 'string' || !request.body.includes('@codex')) { - throw new Error('Authenticated reviewer scope request must bind PR #70'); + throw new Error('Authenticated reviewer scope request must bind PR #92'); } requireCanonicalRole(request.body, 'Independent role', expectedRole); requireCanonicalReviewRequestCommit(request.body, reviewedCommit); diff --git a/scripts/t04-review-thread-blockers.test.mjs b/scripts/t04-review-thread-blockers.test.mjs index dd80e63b9..d68808a77 100644 --- a/scripts/t04-review-thread-blockers.test.mjs +++ b/scripts/t04-review-thread-blockers.test.mjs @@ -33,11 +33,12 @@ test('RI-01 clean verdict requires one canonical findings declaration and no str test('RI-02 authenticated scope request binds PR and exactly one role', () => { const source = 'a'.repeat(40); - const request = { issue_url: 'https://api.github.com/repos/eybersjp/development-kit/issues/70', + const request = { issue_url: 'https://api.github.com/repos/eybersjp/development-kit/issues/92', body: '@codex\nIndependent role: code-reviewer\nCommit: ' + source }; assert.equal(typeof reviewParsers.requireCanonicalGithubReviewRequest, 'function'); assert.doesNotThrow(() => reviewParsers.requireCanonicalGithubReviewRequest(request, 'code-reviewer', source)); - for (const invalid of [{ ...request, issue_url: request.issue_url.replace('/70', '/71') }, + for (const invalid of [{ ...request, issue_url: request.issue_url.replace('/92', '/70') }, + { ...request, issue_url: request.issue_url.replace('/92', '/71') }, { ...request, issue_url: undefined }, { ...request, body: request.body + '\nIndependent role: code-reviewer' }, { ...request, body: request.body + '\nIndependent role: security-reviewer' }]) { @@ -166,7 +167,7 @@ test('T04-A08 generated reviewer request has one immutable SHA and exactly one r const body = formatCanonicalT04ReviewRequest(role, source, 'Inspect precise T04 source and evidence.'); assert.equal((body.match(new RegExp(source, 'g')) || []).length, 1); assert.equal(reviewParsers.requireCanonicalGithubReviewRequest({ - issue_url: 'https://api.github.com/repos/eybersjp/development-kit/issues/70', + issue_url: 'https://api.github.com/repos/eybersjp/development-kit/issues/92', body, }, role, source), source); assert.throws(() => formatCanonicalT04ReviewRequest( diff --git a/scripts/v071-regression.test.mjs b/scripts/v071-regression.test.mjs index 1c6e21024..bbf1d2858 100644 --- a/scripts/v071-regression.test.mjs +++ b/scripts/v071-regression.test.mjs @@ -85,7 +85,16 @@ test('TEST B: First lifecycle interaction -> Require persistent lifecycle state const saved = getCurrentState(rootDir); assert.ok(saved, 'State must be persisted to disk'); assert.equal(saved.currentStage, 'UNDERSTAND'); - assert.ok(existsSync(join(rootDir, '.development-kit', 'autopilot', 'state', 'current.json'))); + // v0.13 fresh projects persist in the canonical append-only ledger, not + // the pre-migration legacy current.json pointer. The round-trip assertion + // above must remain true without inventing a legacy authority file. + const canonicalRoot = join(rootDir, '.development-kit', 'state'); + assert.ok(existsSync(join(canonicalRoot, 'events.jsonl')), + 'Fresh-project lifecycle history must be durably represented in the canonical ledger'); + assert.ok(existsSync(join(canonicalRoot, 'snapshot.json')), + 'The canonical lifecycle snapshot must be materialized'); + assert.equal(existsSync(join(rootDir, '.development-kit', 'autopilot', 'state', 'current.json')), false, + 'Fresh V2 projects must not silently create a legacy current.json authority'); }); test('TEST C: Store a decision naturally -> Destroy in-memory service -> Reinitialize -> Require recall succeeds', async (t) => { From 67570ce802cddb9c76a0bfa6480d092049874a19 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 08:43:39 +0200 Subject: [PATCH 034/177] docs(memory): record v0.13 T04 diagnostic CI, validated port and outstanding risk-3 gates --- memory.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/memory.md b/memory.md index 7147c09e7..3117ff253 100644 --- a/memory.md +++ b/memory.md @@ -2,13 +2,23 @@ ## Published Baseline -- **Current published release candidate:** v0.11.2 (1 October 2026). -- **Package version:** 0.11.2. +- **Current published release baseline:** v0.12.1 (October 2026). +- **Package version:** 0.12.1. - **Core lifecycle:** `UNDERSTAND → DEFINE → DESIGN → PLAN → IMPLEMENT → VERIFY → REVIEW → SIMPLIFY → COMPLETE`. - **Public command surface:** 16 workflow commands. - **Engineering skills:** 48. - **Primary position:** DKF is the reliability control plane for agentic software development — **AI can write it. DKF proves it.** +## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) + +- **Actual integration candidate:** draft PR #92, branch `feature/v0.13-state-engine-v2`, based on released v0.12.1. Historical PR #70 and diagnostics #94–#99 are **not** release merge targets. +- **Test-first diagnostic:** PR #99 reproduced the Windows NTFS case-alias restore-journal bypass (W73-19, CI #41010567167) and orphan displaced-original bypass after journal loss (W73-20, CI #41015062706). Both failed as expected before their narrow corrections. +- **Validated candidate:** PR #99 commit `51556f841bb07e9ed27436e9156bfff022487de1`, CI #41015133002: Ubuntu and Windows v0.13 regression lanes PASS. Earlier W73-19-only candidate also passed both platforms in CI #41014994052. Hosted CI establishes API/test behavior, **not physical NTFS power-loss durability**. +- **Ported to PR #92:** commit `37d1d22d94d2fdf005c35bb1d807c059883fc50b` carries 11 verified source/test/documentation files, excluding PR #99's diagnostic-only workflow exception. PR #92 requires fresh exact-head CI after this port. +- **Safety boundary:** retain original directories, hash-chained restore journals, full Windows legacy-backup restoration, fail-closed publication and recovery, exact-source provenance and v0.12.1 compatibility. +- **Outstanding blockers:** Issue #73 Windows physical crash-durability evidence; independent P1 review findings on Windows DACL inheritance/preservation and recovery publication-barrier retry; Issues #71/#84 acceptance and distinct independent code, architecture and security approvals. No `ACCEPTED` verdict, merge or release is claimed. +- Older v0.11/v0.12 sections below preserve their historical engineering snapshots; their then-current release-status statements do not supersede the v0.12.1 baseline above. + ## v0.11.2 Reliability Controls - Development Contracts with authoritative-source fingerprints. From 4740afdfa7026dde02296dedaa73c4405111f794 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 09:50:24 +0200 Subject: [PATCH 035/177] test(t04): port verified W73-22 Windows native helper fail-closed response regression --- scripts/state-engine-v013.test.mjs | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/scripts/state-engine-v013.test.mjs b/scripts/state-engine-v013.test.mjs index bbd4fb946..562adb449 100644 --- a/scripts/state-engine-v013.test.mjs +++ b/scripts/state-engine-v013.test.mjs @@ -19,6 +19,7 @@ import { migrateLegacyStateToV2, recoverLegacyRestore, restoreLegacyBackup, + validateWindowsDurabilityHelperResult, } from '../runtime/orchestration/state-engine-migration.mjs'; import { createInitialState } from '../runtime/autopilot/transition-model.mjs'; import { saveStateRevision } from '../runtime/autopilot/state-store.mjs'; @@ -519,3 +520,40 @@ test('W73-20 Windows case alias cannot bypass an orphan displaced original after assert.equal(fs.existsSync(aliasRoot), false); assert.equal(fs.readFileSync(path.join(first.displacedOriginal, 'original.txt'), 'utf8'), 'ORIGINAL-ORPHAN'); }); + + +test('W73-22 Windows native helper must prove operation-specific success before advancing recovery', () => { + const probe = { + path: 'C:\\dk\\parent', volumeRoot: 'C:\\', fileSystem: 'NTFS', + driveType: 3, localFixed: true, supportedAutomaticRestore: true, + }; + assert.deepEqual(validateWindowsDurabilityHelperResult('Probe', probe), probe); + assert.throws(() => validateWindowsDurabilityHelperResult('Probe', { + ...probe, supportedAutomaticRestore: false, + }), /unsupported|profile|probe/i); + + const flush = { operation: 'FlushDirectory', profile: probe, flushed: true }; + assert.deepEqual(validateWindowsDurabilityHelperResult('FlushDirectory', flush), flush); + for (const bad of [ + { ...flush, flushed: false }, + { ...flush, flushed: undefined }, + { ...flush, operation: 'Rename' }, + { ...flush, profile: { ...probe, supportedAutomaticRestore: false } }, + ]) assert.throws(() => validateWindowsDurabilityHelperResult('FlushDirectory', bad), + /helper|flush|profile|operation/i); + + const rename = { + operation: 'Rename', source: 'C:\\dk\\from', destination: 'C:\\dk\\to', + profile: probe, renamed: true, parentFlushCompleted: true, + }; + assert.deepEqual(validateWindowsDurabilityHelperResult('Rename', rename), rename); + for (const bad of [ + { ...rename, renamed: false }, + { ...rename, renamed: undefined }, + { ...rename, parentFlushCompleted: false }, + { ...rename, parentFlushCompleted: undefined }, + { ...rename, operation: 'Probe' }, + { ...rename, profile: { ...probe, fileSystem: 'ReFS' } }, + ]) assert.throws(() => validateWindowsDurabilityHelperResult('Rename', bad), + /helper|rename|profile|operation|flush/i); +}); From 8457a59be5e02412393515188d4601757abd0e9e Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 09:50:30 +0200 Subject: [PATCH 036/177] fix(t04): port Ubuntu/Windows validated native helper result verification from PR #102 --- .../orchestration/state-engine-migration.mjs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index 57b20b7db..24cfa5c7f 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -899,6 +899,40 @@ const WINDOWS_DURABILITY_HELPER = path.resolve( '../../scripts/windows-durable-rename.ps1', ); +/** + * A successful PowerShell process exit is not sufficient to certify a durable + * rename/flush. Reject incomplete, mismatched or negative helper responses + * before any restore phase can be acknowledged. This validates API evidence, + * not physical power-loss persistence on a particular NTFS device. + */ +export function validateWindowsDurabilityHelperResult(operation, result) { + const invalid = (reason) => { + throw new StateMigrationError('Windows durability helper returned unverified ' + reason, { + code: 'WINDOWS_DURABILITY_HELPER_INVALID_RESULT', + operation, + }); + }; + if (!result || typeof result !== 'object' || Array.isArray(result)) invalid('result'); + const profile = operation === 'Probe' ? result : result.profile; + if (!profile || profile.supportedAutomaticRestore !== true + || profile.localFixed !== true + || String(profile.fileSystem || '').toUpperCase() !== 'NTFS') { + invalid('storage profile'); + } + if (operation === 'Probe') return result; + if (result.operation !== operation) invalid('operation'); + if (operation === 'FlushDirectory') { + if (result.flushed !== true) invalid('directory flush'); + } else if (operation === 'Rename') { + if (result.renamed !== true || result.parentFlushCompleted !== true) { + invalid('rename or parent-directory flush'); + } + } else { + invalid('operation'); + } + return result; +} + function runWindowsDurabilityHelper(operation, args = {}) { if (process.platform !== 'win32') { throw new StateMigrationError('Windows durability helper may only run on Windows'); @@ -942,14 +976,16 @@ function runWindowsDurabilityHelper(operation, args = {}) { if (!output) { throw new StateMigrationError('Windows durability helper returned no result'); } + let parsed; try { - return JSON.parse(output.split(/\r?\n/).filter(Boolean).at(-1)); + parsed = JSON.parse(output.split(/\r?\n/).filter(Boolean).at(-1)); } catch (error) { throw new StateMigrationError('Windows durability helper returned invalid JSON', { operation, cause: error.message, }); } + return validateWindowsDurabilityHelperResult(operation, parsed); } function requireSupportedWindowsRestoreProfile(targetPath) { From a580c6e2845d743149d1b0e164f2d1b30ef4d3a0 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 09:50:41 +0200 Subject: [PATCH 037/177] docs(memory): record source-bound T04 W73-22 red/green CI and PR #92 port boundaries --- memory.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/memory.md b/memory.md index 3117ff253..b0285b96d 100644 --- a/memory.md +++ b/memory.md @@ -14,7 +14,8 @@ - **Actual integration candidate:** draft PR #92, branch `feature/v0.13-state-engine-v2`, based on released v0.12.1. Historical PR #70 and diagnostics #94–#99 are **not** release merge targets. - **Test-first diagnostic:** PR #99 reproduced the Windows NTFS case-alias restore-journal bypass (W73-19, CI #41010567167) and orphan displaced-original bypass after journal loss (W73-20, CI #41015062706). Both failed as expected before their narrow corrections. - **Validated candidate:** PR #99 commit `51556f841bb07e9ed27436e9156bfff022487de1`, CI #41015133002: Ubuntu and Windows v0.13 regression lanes PASS. Earlier W73-19-only candidate also passed both platforms in CI #41014994052. Hosted CI establishes API/test behavior, **not physical NTFS power-loss durability**. -- **Ported to PR #92:** commit `37d1d22d94d2fdf005c35bb1d807c059883fc50b` carries 11 verified source/test/documentation files, excluding PR #99's diagnostic-only workflow exception. PR #92 requires fresh exact-head CI after this port. +- **Ported to PR #92:** commit `37d1d22d94d2fdf005c35bb1d807c059883fc50b` carries 11 verified source/test/documentation files, excluding PR #99's diagnostic-only workflow exception. Exact-head CI #41015777874 subsequently passed both Ubuntu and Windows v0.13 lanes. +- **Windows helper result contract W73-22 (test-first, 30 October):** draft diagnostic PR #102 at negative-control `ab10a41f8292d7bf0f4b1e572f709002c5b5984c` failed Ubuntu and Windows in CI #41021958044 because the helper-response verifier was absent; corrected diagnostic `7cb6ca4d04fc5dd651bc4050bb4c5128255ade11` passed both full v0.13 lanes in CI #41022103181. The source/test-only correction was ported to PR #92 in commits `4740afdfa7026dde02296dedaa73c4405111f794` and `8457a59be5e02412393515188d4601757abd0e9e`. Exit-zero native helper results now require affirmative operation-specific flush/rename flags and supported NTFS profile. PR #102 is diagnostic only; its CI workflow exception was not ported. Fresh exact-head PR #92 CI and independent review remain required. This is API result verification, **not** physical power-loss proof. - **Safety boundary:** retain original directories, hash-chained restore journals, full Windows legacy-backup restoration, fail-closed publication and recovery, exact-source provenance and v0.12.1 compatibility. - **Outstanding blockers:** Issue #73 Windows physical crash-durability evidence; independent P1 review findings on Windows DACL inheritance/preservation and recovery publication-barrier retry; Issues #71/#84 acceptance and distinct independent code, architecture and security approvals. No `ACCEPTED` verdict, merge or release is claimed. - Older v0.11/v0.12 sections below preserve their historical engineering snapshots; their then-current release-status statements do not supersede the v0.12.1 baseline above. From 8bf7f698a54b0fb0a4317391d32cc1d811fda318 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 11:04:01 +0200 Subject: [PATCH 038/177] fix(v0.13 T04): port tested private NTFS stage, ACL preservation and helper identity evidence (scripts/windows-durable-rename.ps1) --- scripts/windows-durable-rename.ps1 | 204 ++++++++++++++++++++++++++++- 1 file changed, 203 insertions(+), 1 deletion(-) diff --git a/scripts/windows-durable-rename.ps1 b/scripts/windows-durable-rename.ps1 index 613471cae..1f3d5641f 100644 --- a/scripts/windows-durable-rename.ps1 +++ b/scripts/windows-durable-rename.ps1 @@ -10,7 +10,7 @@ # arguments; this script never evaluates command text or interpolates a shell. param( [Parameter(Mandatory = $true)] - [ValidateSet('Probe', 'Rename', 'FlushDirectory')] + [ValidateSet('Probe', 'Rename', 'FlushDirectory', 'CreateSecureStage', 'CopyAclTree')] [string]$Operation, [string]$Path, @@ -86,6 +86,71 @@ public static class DkfWindowsDurability { [return: MarshalAs(UnmanagedType.Bool)] private static extern bool FlushFileBuffers(SafeFileHandle handle); + // SetNamedSecurityInfoW applies NTFS auto-inheritance; .NET SetAccessControl + // may clear SE_DACL_AUTO_INHERITED even when all current ACEs match. + private const uint SE_FILE_OBJECT = 1; + private const uint DACL_SECURITY_INFORMATION = 0x00000004; + private const uint PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000; + private const uint UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000; + + [DllImport("advapi32.dll", EntryPoint = "GetNamedSecurityInfoW", + CharSet = CharSet.Unicode)] + private static extern uint GetNamedSecurityInfo( + string name, uint objectType, uint securityInfo, + out IntPtr owner, out IntPtr group, out IntPtr dacl, + out IntPtr sacl, out IntPtr securityDescriptor); + + [DllImport("advapi32.dll", EntryPoint = "SetNamedSecurityInfoW", + CharSet = CharSet.Unicode)] + private static extern uint SetNamedSecurityInfo( + string name, uint objectType, uint securityInfo, + IntPtr owner, IntPtr group, IntPtr dacl, IntPtr sacl); + + // Legacy DACLs without SE_DACL_AUTO_INHERITED must not be silently + // converted into auto-inheriting descriptors by SetNamedSecurityInfoW. + // SetFileSecurityW is used only for legacy DACLs without the + // auto-inherited control bit; exact protected status is verified after. + [DllImport("advapi32.dll", EntryPoint = "SetFileSecurityW", + CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetFileSecurity( + string name, uint securityInfo, IntPtr securityDescriptor); + + [DllImport("kernel32.dll", EntryPoint = "LocalFree")] + private static extern IntPtr LocalFree(IntPtr handle); + + public static void CopyDaclWithInheritance(string from, string to, bool protectedDacl, bool autoInherited) { + IntPtr owner, group, dacl, sacl, descriptor; + uint status = GetNamedSecurityInfo(from, SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, out owner, out group, out dacl, + out sacl, out descriptor); + if (status != 0) { + throw new Win32Exception((int)status, "GetNamedSecurityInfoW source DACL failed"); + } + try { + // Never install a NULL DACL: it grants unrestricted access. + if (dacl == IntPtr.Zero) { + throw new InvalidOperationException("Source has a NULL DACL; refusing staged ACL publication"); + } + if (autoInherited) { + uint flags = DACL_SECURITY_INFORMATION | + (protectedDacl ? PROTECTED_DACL_SECURITY_INFORMATION : + UNPROTECTED_DACL_SECURITY_INFORMATION); + status = SetNamedSecurityInfo(to, SE_FILE_OBJECT, flags, + IntPtr.Zero, IntPtr.Zero, dacl, IntPtr.Zero); + if (status != 0) { + throw new Win32Exception((int)status, "SetNamedSecurityInfoW staged DACL failed"); + } + } else { + if (!SetFileSecurity(to, DACL_SECURITY_INFORMATION, descriptor)) { + throw LastError("SetFileSecurityW legacy DACL copy failed"); + } + } + } finally { + if (descriptor != IntPtr.Zero) LocalFree(descriptor); + } + } + private static Win32Exception LastError(string operation) { return new Win32Exception(Marshal.GetLastWin32Error(), operation); } @@ -168,7 +233,144 @@ function Get-DkfStorageProfile([string]$InputPath) { } } + +# Windows may clear the SE_DACL_AUTO_INHERITED control bit on a protected +# DACL when applying an otherwise identical descriptor to a new object. +# Compare protection and ACE SDDL exactly. The D:AI auto-inheritance +# control flag can affect future child inheritance, so it must not be +# normalized away merely because existing ACEs match. +function Get-DkfComparableDacl([System.Security.AccessControl.ObjectSecurity]$Acl) { + $section = [System.Security.AccessControl.AccessControlSections]::Access + $sddl = $Acl.GetSecurityDescriptorSddlForm($section) + return $sddl +} + +# Create a protected staging root with its DACL supplied atomically at mkdir +# time. A sibling's inherited ACL must never expose copied project data. +function New-DkfSecureStage([string]$StagePath, [string]$SourcePath) { + if ([string]::IsNullOrWhiteSpace($StagePath)) { throw 'Stage path is required' } + $stage = [System.IO.Path]::GetFullPath($StagePath) + $parent = [System.IO.Path]::GetDirectoryName($stage) + if (-not [System.IO.Directory]::Exists($parent)) { throw 'Stage parent is missing' } + $profile = Get-DkfStorageProfile $parent + if (-not $profile.supportedAutomaticRestore) { throw 'Secure staging requires local fixed NTFS' } + if ([System.IO.Directory]::Exists($stage) -or [System.IO.File]::Exists($stage)) { + throw 'Secure stage already exists' + } + + # Supply a private maintenance-only DACL *at creation*, never inherit the + # sibling parent's potentially wider DACL while private data is staged. + $privateAcl = New-Object System.Security.AccessControl.DirectorySecurity + $privateAcl.SetAccessRuleProtection($true, $false) + $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User + $rule = [System.Security.AccessControl.FileSystemAccessRule]::new( + $sid, + [System.Security.AccessControl.FileSystemRights]::FullControl, + ([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor + [System.Security.AccessControl.InheritanceFlags]::ObjectInherit), + [System.Security.AccessControl.PropagationFlags]::None, + [System.Security.AccessControl.AccessControlType]::Allow) + $privateAcl.AddAccessRule($rule) + [System.IO.Directory]::CreateDirectory($stage, $privateAcl) | Out-Null + + if (-not [string]::IsNullOrWhiteSpace($SourcePath) -and + -not [System.IO.Directory]::Exists($SourcePath)) { + throw 'Original source root is missing before staging' + } + # The original root may grant broad access while individual children + # contain confidential ACLs. Never widen the stage root before the + # entire copied tree has received and verified its original child DACLs. + $actual = [System.IO.Directory]::GetAccessControl($stage) + if (-not $actual.AreAccessRulesProtected) { + throw 'Private stage DACL unexpectedly inherits parent access' + } + $rules = $actual.GetAccessRules($true, $true, + [System.Security.Principal.SecurityIdentifier]) + if ($rules.Count -lt 1) { throw 'Private stage has no maintenance ACL' } + foreach ($entry in $rules) { + if ($entry.IsInherited -or $entry.IdentityReference.Value -ne $sid.Value -or + $entry.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) { + throw 'Private stage has unexpected additional or inherited access' + } + } + return $stage +} + +# Post-order: descendants are copied while the staging root remains private. +# Copy exact source DACLs only after all copied bytes are present. Any failure +# prevents promotion and leaves the original untouched. +function Copy-DkfAclTree([string]$Source, [string]$Destination, [bool]$VerifyOnly = $false) { + $src = Get-Item -LiteralPath $Source -Force -ErrorAction Stop + $dst = Get-Item -LiteralPath $Destination -Force -ErrorAction Stop + $srcReparse = ($src.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 + $dstReparse = ($dst.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 + if ($srcReparse -or $dstReparse) { + # Do not follow symlinks or junctions while traversing source ACLs. + # Existing relative symbolic links are copied verbatim by fs.cpSync; + # their containing directory remains protected by its source DACL. + if ($srcReparse -and $dstReparse -and + $src.LinkType -eq 'SymbolicLink' -and $dst.LinkType -eq 'SymbolicLink') { + return + } + throw 'Unsupported or mismatched reparse point during NTFS ACL preservation' + } + if ($src.PSIsContainer -ne $dst.PSIsContainer) { + throw 'Source and staged object kinds differ during ACL preservation' + } + if ($src.PSIsContainer) { + foreach ($child in Get-ChildItem -LiteralPath $Source -Force -ErrorAction Stop) { + Copy-DkfAclTree (Join-Path $Source $child.Name) (Join-Path $Destination $child.Name) $VerifyOnly + } + } + if ($src.PSIsContainer) { + $acl = [System.IO.Directory]::GetAccessControl($Source) + if (-not $VerifyOnly) { [DkfWindowsDurability]::CopyDaclWithInheritance($Source, $Destination, $acl.AreAccessRulesProtected, ($acl.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access) -match '^D:(?:P)?AI')) } + $actualAcl = [System.IO.Directory]::GetAccessControl($Destination) + } else { + $acl = [System.IO.File]::GetAccessControl($Source) + if (-not $VerifyOnly) { [DkfWindowsDurability]::CopyDaclWithInheritance($Source, $Destination, $acl.AreAccessRulesProtected, ($acl.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access) -match '^D:(?:P)?AI')) } + $actualAcl = [System.IO.File]::GetAccessControl($Destination) + } + $sections = [System.Security.AccessControl.AccessControlSections]::Access + $expected = $acl.GetSecurityDescriptorSddlForm($sections) + $actual = $actualAcl.GetSecurityDescriptorSddlForm($sections) + if ($expected -cne $actual) { + throw ('Staged NTFS DACL does not match source after copy: source={0} staged={1}' -f $expected, $actual) + } +} + switch ($Operation) { + + 'CreateSecureStage' { + $created = New-DkfSecureStage $Path $From + [PSCustomObject]@{ operation = 'CreateSecureStage'; path = $created; protected = $true } | + ConvertTo-Json -Compress + break + } + + 'CopyAclTree' { + if ([string]::IsNullOrWhiteSpace($From) -or [string]::IsNullOrWhiteSpace($To)) { + throw 'Source and stage paths are required' + } + $sourcePath = [System.IO.Path]::GetFullPath($From) + $destinationPath = [System.IO.Path]::GetFullPath($To) + $sourceProfile = Get-DkfStorageProfile $sourcePath + $destinationProfile = Get-DkfStorageProfile $destinationPath + if (-not $sourceProfile.supportedAutomaticRestore -or + -not $destinationProfile.supportedAutomaticRestore -or + $sourceProfile.volumeRoot -ne $destinationProfile.volumeRoot) { + throw 'NTFS ACL copy requires local fixed same-volume source and stage' + } + Copy-DkfAclTree $sourcePath $destinationPath + # A parent DACL change can propagate to already-copied descendants. + # Verify the entire tree again after all parent operations complete. + Copy-DkfAclTree $sourcePath $destinationPath $true + [PSCustomObject]@{ operation = 'CopyAclTree'; source = $sourcePath; + destination = $destinationPath; verified = $true } | ConvertTo-Json -Compress + break + } + + 'Probe' { $profile = Get-DkfStorageProfile $Path $profile | ConvertTo-Json -Compress From 132907dc5dbf7a33e9662491954555c8b708c736 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 11:04:08 +0200 Subject: [PATCH 039/177] fix(v0.13 T04): port tested private NTFS stage, ACL preservation and helper identity evidence (runtime/orchestration/state-engine-migration.mjs) --- .../orchestration/state-engine-migration.mjs | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index 24cfa5c7f..96b80a171 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -905,14 +905,52 @@ const WINDOWS_DURABILITY_HELPER = path.resolve( * before any restore phase can be acknowledged. This validates API evidence, * not physical power-loss persistence on a particular NTFS device. */ -export function validateWindowsDurabilityHelperResult(operation, result) { +export function validateWindowsDurabilityHelperResult(operation, result, expected = {}) { const invalid = (reason) => { throw new StateMigrationError('Windows durability helper returned unverified ' + reason, { code: 'WINDOWS_DURABILITY_HELPER_INVALID_RESULT', operation, + diagnostic: operation === 'CreateSecureStage' ? { + observedOperation: result?.operation, + observedPath: result?.path, + expectedPath: expected?.Path, + observedProtected: result?.protected, + } : undefined, }); }; if (!result || typeof result !== 'object' || Array.isArray(result)) invalid('result'); + // ACL helper calls are path-scoped and have no storage-profile field. The + // parent restore path was separately probed; accept only an affirmative + // response for the exact operation and expected source/destination paths. + const samePath = (actual, requested) => { + if (typeof actual !== 'string' || typeof requested !== 'string') return false; + if (path.win32.normalize(actual).toLowerCase() === + path.win32.normalize(requested).toLowerCase()) return true; + // Windows may expand an 8.3 path component (RUNNER~1 -> runneradmin) + // while producing the helper response. Require both spellings to resolve + // to the same ordinary directory identity; never accept mere textual + // similarity, a reparse point, or a nonexistent destination. + try { + const a = fs.lstatSync(actual, { bigint: true }); + const b = fs.lstatSync(requested, { bigint: true }); + return a.isDirectory() && b.isDirectory() && + !a.isSymbolicLink() && !b.isSymbolicLink() && + a.ino !== 0n && a.dev === b.dev && a.ino === b.ino; + } catch { + return false; + } + }; + if (operation === 'CreateSecureStage') { + if (result.operation !== operation || result.protected !== true || + !samePath(result.path, expected.Path)) invalid('private stage or path'); + return result; + } + if (operation === 'CopyAclTree') { + if (result.operation !== operation || result.verified !== true || + !samePath(result.source, expected.From) || + !samePath(result.destination, expected.To)) invalid('ACL tree verification or path'); + return result; + } const profile = operation === 'Probe' ? result : result.profile; if (!profile || profile.supportedAutomaticRestore !== true || profile.localFixed !== true @@ -985,7 +1023,7 @@ function runWindowsDurabilityHelper(operation, args = {}) { cause: error.message, }); } - return validateWindowsDurabilityHelperResult(operation, parsed); + return validateWindowsDurabilityHelperResult(operation, parsed, args); } function requireSupportedWindowsRestoreProfile(targetPath) { @@ -1504,14 +1542,23 @@ export function restoreLegacyBackup({ const stage = path.join(parent, '.dk-legacy-restore-stage-' + restoreJournalDigest(resolvedRoot) + '-' + transactionId); const displaced = stage + '-original'; - fs.mkdirSync(stage, { mode: 0o700 }); + if (process.platform === 'win32') { + // Create a private protected NTFS DACL atomically. Keep it private while + // copying sensitive descendants, then restore exact source ACLs in + // post-order before publishing the candidate. + runWindowsDurabilityHelper('CreateSecureStage', { + Path: stage, ...(originalRootExists ? { From: resolvedRoot } : {}), + }); + } else { + fs.mkdirSync(stage, { mode: 0o700 }); + } let promoted = false; let displacedOriginal = false; let transaction = null; try { - fs.chmodSync(stage, 0o700); + if (process.platform !== 'win32') fs.chmodSync(stage, 0o700); if (originalRootExists) { fs.cpSync(resolvedRoot, stage, { recursive: true, @@ -1542,6 +1589,13 @@ export function restoreLegacyBackup({ syncRestoreTree(stage); if (rootStat) applyRestoreMetadata(stage, originalMetadata); + if (process.platform === 'win32' && originalRootExists) { + // Child ACLs are installed while the stage root is still private. + // CopyAclTree applies the root ACL last, then re-verifies the whole + // tree so inherited ACE propagation cannot silently widen children. + runWindowsDurabilityHelper('CopyAclTree', { From: resolvedRoot, To: stage }); + syncRestoreTree(stage); + } const stageStat = fs.lstatSync(stage, { bigint: true }); transaction = { From 11c2843bef8b8363fa39ff248ead38de705ebef0 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 11:04:16 +0200 Subject: [PATCH 040/177] fix(v0.13 T04): port tested private NTFS stage, ACL preservation and helper identity evidence (scripts/state-engine-v013.test.mjs) --- scripts/state-engine-v013.test.mjs | 188 +++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/scripts/state-engine-v013.test.mjs b/scripts/state-engine-v013.test.mjs index 562adb449..3a62041c2 100644 --- a/scripts/state-engine-v013.test.mjs +++ b/scripts/state-engine-v013.test.mjs @@ -4,6 +4,7 @@ import fs from 'node:fs'; import crypto from 'node:crypto'; import os from 'node:os'; import path from 'node:path'; +import { spawnSync } from 'node:child_process'; import { appendMetadataEvent, @@ -557,3 +558,190 @@ test('W73-22 Windows native helper must prove operation-specific success before ]) assert.throws(() => validateWindowsDurabilityHelperResult('Rename', bad), /helper|rename|profile|operation|flush/i); }); + +test('W73-21 Windows restore stage and promoted root preserve the original restrictive NTFS DACL', (t) => { + if (process.platform !== 'win32') { + t.skip('NTFS ACL fixture requires Windows'); return; + } + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-ntfs-acl-'); + const targetRoot = path.join(parent, 'restricted-project'); + fs.mkdirSync(targetRoot); + fs.writeFileSync(path.join(targetRoot, 'confidential.txt'), 'CONFIDENTIAL-ORIGINAL'); + + function aclScript(script, location) { + return spawnSync('powershell.exe', + ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], + { encoding: 'utf8', windowsHide: true, shell: false, timeout: 30000, + env: { ...process.env, DKF_TEST_ACL_PATH: location } }); + } + const restrict = aclScript([ + "$ErrorActionPreference='Stop'", + '$acl=[System.IO.Directory]::GetAccessControl($env:DKF_TEST_ACL_PATH)', + '$acl.SetAccessRuleProtection($true,$false)', + '$sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User', + '$rights=[System.Security.AccessControl.FileSystemRights]::FullControl', + '$flags=[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit', + '$rule=[System.Security.AccessControl.FileSystemAccessRule]::new($sid,$rights,$flags,[System.Security.AccessControl.PropagationFlags]::None,[System.Security.AccessControl.AccessControlType]::Allow)', + '$acl.SetAccessRule($rule)', + '[System.IO.Directory]::SetAccessControl($env:DKF_TEST_ACL_PATH,$acl)', + ].join(';'), targetRoot); + if (restrict.status !== 0) { + t.skip('Host cannot create restrictive NTFS DACL fixture: ' + String(restrict.stderr).slice(0, 200)); + return; + } + function dacl(location) { + const result = aclScript( + "$ErrorActionPreference='Stop'; ([System.IO.Directory]::GetAccessControl($env:DKF_TEST_ACL_PATH)).GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access)", + location, + ); + assert.equal(result.status, 0, String(result.stderr)); + return result.stdout.trim(); + } + const comparableDacl = (value) => value; + const originalDacl = dacl(targetRoot); + const fileDacl = (location) => { + const result = aclScript( + "$ErrorActionPreference='Stop'; ([System.IO.File]::GetAccessControl($env:DKF_TEST_ACL_PATH)).GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access)", + location, + ); + assert.equal(result.status, 0, String(result.stderr)); + return result.stdout.trim(); + }; + const originalFileDacl = fileDacl(path.join(targetRoot, 'confidential.txt')); + assert.notEqual(originalDacl, dacl(parent), 'Fixture must differ from inherited parent DACL'); + + const originalCp = fs.cpSync; + let stageInspected = false; + fs.cpSync = function verifyPrivateStageBeforeCopy(source, destination, ...options) { + if (path.resolve(source) === path.resolve(targetRoot)) { + stageInspected = true; + assert.match(dacl(destination), /^D:P/, + 'Restore stage must be private and protected before sensitive data copy'); + } + return originalCp.call(this, source, destination, ...options); + }; + try { + try { + restoreLegacyBackup({ backupPath, targetRoot, overwrite: true, confirmOffline: true }); + } catch (error) { + assert.fail('W73-21 restore error: ' + error.message + ' details=' + JSON.stringify(error.details)); + } + } finally { + fs.cpSync = originalCp; + } + assert.equal(stageInspected, true); + assert.equal(comparableDacl(dacl(targetRoot)), comparableDacl(originalDacl), + 'Promoted root must preserve source DACL protection and ACEs'); + assert.equal(comparableDacl(fileDacl(path.join(targetRoot, 'confidential.txt'))), + comparableDacl(originalFileDacl), + 'Original file DACL must survive staged copy and promotion'); + assert.equal(fs.readFileSync(path.join(targetRoot, 'confidential.txt'), 'utf8'), 'CONFIDENTIAL-ORIGINAL'); +}); + +test('W73-23 native ACL helper responses must be explicit and path-bound', () => { + const stage = { operation: 'CreateSecureStage', path: 'C:\\dk\\stage', protected: true }; + assert.deepEqual(validateWindowsDurabilityHelperResult('CreateSecureStage', stage, + { Path: 'C:\\dk\\stage' }), stage); + for (const invalid of [ + { ...stage, protected: false }, + { ...stage, path: 'C:\\dk\\other' }, + { ...stage, operation: 'Rename' }, + ]) { + assert.throws(() => validateWindowsDurabilityHelperResult('CreateSecureStage', invalid, + { Path: 'C:\\dk\\stage' }), /helper|stage|path|operation|protect/i); + } + + const tree = { operation: 'CopyAclTree', source: 'C:\\dk\\source', + destination: 'C:\\dk\\stage', verified: true }; + assert.deepEqual(validateWindowsDurabilityHelperResult('CopyAclTree', tree, + { From: 'C:\\dk\\source', To: 'C:\\dk\\stage' }), tree); + for (const invalid of [ + { ...tree, verified: false }, + { ...tree, source: 'C:\\dk\\wrong' }, + { ...tree, destination: 'C:\\dk\\wrong' }, + { ...tree, operation: 'Rename' }, + ]) { + assert.throws(() => validateWindowsDurabilityHelperResult('CopyAclTree', invalid, + { From: 'C:\\dk\\source', To: 'C:\\dk\\stage' }), + /helper|acl|path|operation|source|destination/i); + } +}); + + +test('W73-24 staged confidential child is never copied under a broader root DACL', (t) => { + if (process.platform !== 'win32') { + t.skip('NTFS DACL isolation requires Windows'); return; + } + const { backupPath } = createLegacyBackupFixture(t); + const parent = tempDir(t, 'dk-v013-ntfs-private-stage-'); + const targetRoot = path.join(parent, 'mixed-access-project'); + const child = path.join(targetRoot, 'confidential.txt'); + fs.mkdirSync(targetRoot); + fs.writeFileSync(child, 'CONFIDENTIAL-ORIGINAL'); + + function ps(script, location) { + return spawnSync('powershell.exe', + ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], + { encoding: 'utf8', windowsHide: true, shell: false, timeout: 30000, + env: { ...process.env, DKF_TEST_ACL_PATH: location } }); + } + const aclSetup = ps([ + "$ErrorActionPreference='Stop'", + '$root=$env:DKF_TEST_ACL_PATH', + '$user=[System.Security.Principal.WindowsIdentity]::GetCurrent().User', + '$everyone=[System.Security.Principal.SecurityIdentifier]::new("S-1-1-0")', + '$full=[System.Security.AccessControl.FileSystemRights]::FullControl', + '$read=[System.Security.AccessControl.FileSystemRights]::ReadAndExecute', + '$inherit=[System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit', + '$allow=[System.Security.AccessControl.AccessControlType]::Allow', + '$none=[System.Security.AccessControl.PropagationFlags]::None', + '$acl=[System.IO.Directory]::GetAccessControl($root)', + '$acl.SetAccessRuleProtection($true,$false)', + '$acl.SetAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new($user,$full,$inherit,$none,$allow))', + '$acl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new($everyone,$read,$inherit,$none,$allow))', + '[System.IO.Directory]::SetAccessControl($root,$acl)', + '$file=[System.IO.File]::GetAccessControl([System.IO.Path]::Combine($root,"confidential.txt"))', + '$file.SetAccessRuleProtection($true,$false)', + '$file.SetAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new($user,$full,$allow))', + '[System.IO.File]::SetAccessControl([System.IO.Path]::Combine($root,"confidential.txt"),$file)', + ].join(';'), targetRoot); + assert.equal(aclSetup.status, 0, 'Windows DACL fixture must execute: ' + String(aclSetup.stderr)); + function dacl(location, kind = 'Directory') { + const z = ps("$ErrorActionPreference='Stop'; ([System.IO." + kind + + "]::GetAccessControl($env:DKF_TEST_ACL_PATH)).GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access)", + location); + assert.equal(z.status, 0, String(z.stderr)); + return z.stdout.trim(); + } + const rootAcl = dacl(targetRoot); + const childAcl = dacl(child, 'File'); + assert.match(rootAcl, /;;;WD\)/, 'Fixture root must grant Everyone read access'); + assert.doesNotMatch(childAcl, /;;;WD\)/, 'Fixture confidential child must deny Everyone access'); + + const originalCp = fs.cpSync; + let inspected = false; + fs.cpSync = function assertPrivateStageDuringCopy(source, destination, ...args) { + if (path.resolve(source) === path.resolve(targetRoot)) { + inspected = true; + const before = dacl(destination); + assert.match(before, /^D:P/, 'Stage root must have protected private DACL before copy'); + assert.doesNotMatch(before, /;;;WD\)/, 'Stage root must not expose confidential children'); + const result = originalCp.call(this, source, destination, ...args); + const during = dacl(destination); + assert.match(during, /^D:P/, 'Stage must remain private until child ACLs are restored'); + assert.doesNotMatch(during, /;;;WD\)/, 'Copied confidential bytes must remain private'); + return result; + } + return originalCp.call(this, source, destination, ...args); + }; + try { + restoreLegacyBackup({ backupPath, targetRoot, overwrite: true, confirmOffline: true }); + } finally { + fs.cpSync = originalCp; + } + assert.equal(inspected, true, 'Test must observe the real copy boundary'); + assert.equal(dacl(targetRoot), rootAcl, 'Published root must preserve the original DACL'); + assert.equal(dacl(path.join(targetRoot, 'confidential.txt'), 'File'), childAcl, + 'Published confidential child must preserve its more restrictive DACL'); +}); From 1f2c909816287aeceb7aca1f2fd4e6dd9a68aadf Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 11:04:25 +0200 Subject: [PATCH 041/177] docs(memory): record test-first NTFS DACL fix, evidence and remaining T04 gates --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index b0285b96d..15de6bb56 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **NTFS DACL private-stage hardening (30 October, W73-21/23/24):** diagnostic draft PR #104 reproduced missing ACL preservation and unverified native-helper responses on exact negative-control source (CI #41029403553). The v0.13 Windows helper now creates a maintenance-only protected stage at creation and retains its private root DACL throughout sensitive file copy; descendant original DACLs are restored post-order and whole-tree verified before the original root DACL is installed last. Native helper result checks bind operations to the actual directory identity (including verified NTFS 8.3 short-path aliases) and reject false/mismatched results. Focused real Windows NTFS W73-21/23/24 plus legacy restore fixtures passed with zero skips in CI #41029930153; Ubuntu full validation passed in CI #41029930369, with Windows full compatibility still pending at the time of this memory update. Ported only source/test files from diagnostic PR #104 into integration PR #92 through commit `11c2843bef8b8363fa39ff248ead38de705ebef0`; diagnostic workflow exceptions were NOT ported. **This is not NTFS physical power-loss proof or risk-3 ACCEPTED.** Issue #73 and three-role independent reviews remain blocking. + - **Actual integration candidate:** draft PR #92, branch `feature/v0.13-state-engine-v2`, based on released v0.12.1. Historical PR #70 and diagnostics #94–#99 are **not** release merge targets. - **Test-first diagnostic:** PR #99 reproduced the Windows NTFS case-alias restore-journal bypass (W73-19, CI #41010567167) and orphan displaced-original bypass after journal loss (W73-20, CI #41015062706). Both failed as expected before their narrow corrections. - **Validated candidate:** PR #99 commit `51556f841bb07e9ed27436e9156bfff022487de1`, CI #41015133002: Ubuntu and Windows v0.13 regression lanes PASS. Earlier W73-19-only candidate also passed both platforms in CI #41014994052. Hosted CI establishes API/test behavior, **not physical NTFS power-loss durability**. From ad986f4ec24b79061a42ea2f3328bdf6f12162d2 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 11:21:37 +0200 Subject: [PATCH 042/177] docs(v0.13): record exact-head cross-platform T04 evidence and unresolved acceptance boundaries --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index 15de6bb56..b67239f3f 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **30 October exact-source verification checkpoint:** integration PR #92 head `1f2c909816287aeceb7aca1f2fd4e6dd9a68aadf` passed GitHub Actions CI #41030766378 on **both Ubuntu and Windows** (two platform jobs in one workflow execution; NOT two independent full runs). The diagnostic PR #99 head `51556f841bb07e9ed27436e9156bfff022487de1` passed both platform jobs in CI #41015133002; the diagnostic head is divergent and must not be merged as a substitute for PR #92. W73-21/23/24 NTFS ACL and helper-identity corrections are integrated on PR #92. Current acceptance blockers remain **controlled NTFS physical power-loss evidence under Issue #73, distinct authenticated exact-source code/architecture/security reviews, repeated complete platform validation on the final reviewed SHA, and persisted deterministic risk-3 ACCEPTED**. No acceptance, release or merge is asserted. Updating this file creates a new source commit and therefore requires fresh CI and review binding. + - **NTFS DACL private-stage hardening (30 October, W73-21/23/24):** diagnostic draft PR #104 reproduced missing ACL preservation and unverified native-helper responses on exact negative-control source (CI #41029403553). The v0.13 Windows helper now creates a maintenance-only protected stage at creation and retains its private root DACL throughout sensitive file copy; descendant original DACLs are restored post-order and whole-tree verified before the original root DACL is installed last. Native helper result checks bind operations to the actual directory identity (including verified NTFS 8.3 short-path aliases) and reject false/mismatched results. Focused real Windows NTFS W73-21/23/24 plus legacy restore fixtures passed with zero skips in CI #41029930153; Ubuntu full validation passed in CI #41029930369, with Windows full compatibility still pending at the time of this memory update. Ported only source/test files from diagnostic PR #104 into integration PR #92 through commit `11c2843bef8b8363fa39ff248ead38de705ebef0`; diagnostic workflow exceptions were NOT ported. **This is not NTFS physical power-loss proof or risk-3 ACCEPTED.** Issue #73 and three-role independent reviews remain blocking. - **Actual integration candidate:** draft PR #92, branch `feature/v0.13-state-engine-v2`, based on released v0.12.1. Historical PR #70 and diagnostics #94–#99 are **not** release merge targets. From 6b89d9dba299b05c438480c647038590ba2d27e9 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 13:01:38 +0200 Subject: [PATCH 043/177] fix(v0.13 T04): port CI-validated W73-25 Windows helper path/alias binding from diagnostic PR #105 --- .../orchestration/state-engine-migration.mjs | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index 96b80a171..767e5a549 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -928,18 +928,33 @@ export function validateWindowsDurabilityHelperResult(operation, result, expecte path.win32.normalize(requested).toLowerCase()) return true; // Windows may expand an 8.3 path component (RUNNER~1 -> runneradmin) // while producing the helper response. Require both spellings to resolve - // to the same ordinary directory identity; never accept mere textual - // similarity, a reparse point, or a nonexistent destination. + // to the same ordinary directory or file identity; never accept mere + // textual similarity, a reparse point, or a nonexistent destination. try { const a = fs.lstatSync(actual, { bigint: true }); const b = fs.lstatSync(requested, { bigint: true }); - return a.isDirectory() && b.isDirectory() && - !a.isSymbolicLink() && !b.isSymbolicLink() && + // Rename also applies to the retained V1 journal file, not only to + // directories. Both aliases must identify the same ordinary object + // of the same kind; never follow a symlink or accept a missing path. + const sameKind = (a.isDirectory() && b.isDirectory()) || + (a.isFile() && b.isFile()); + return sameKind && !a.isSymbolicLink() && !b.isSymbolicLink() && a.ino !== 0n && a.dev === b.dev && a.ino === b.ino; } catch { return false; } }; + // After a successful rename the source no longer exists, so its inode + // cannot be compared directly. An expanded Windows 8.3 ancestor alias is + // safe to recognize by the identical leaf name and the live identity of + // the source parent directory. Never accept a different leaf or parent. + const sameRenamedSource = (actual, requested) => { + if (samePath(actual, requested)) return true; + if (typeof actual !== 'string' || typeof requested !== 'string') return false; + return path.win32.basename(actual).toLowerCase() === + path.win32.basename(requested).toLowerCase() && + samePath(path.win32.dirname(actual), path.win32.dirname(requested)); + }; if (operation === 'CreateSecureStage') { if (result.operation !== operation || result.protected !== true || !samePath(result.path, expected.Path)) invalid('private stage or path'); @@ -957,11 +972,32 @@ export function validateWindowsDurabilityHelperResult(operation, result, expecte || String(profile.fileSystem || '').toUpperCase() !== 'NTFS') { invalid('storage profile'); } - if (operation === 'Probe') return result; + // The helper's success flags alone are insufficient: the response must + // attest to the exact paths requested for this transaction. On Windows, + // samePath also accepts a verified ordinary-directory 8.3 alias. + if (operation === 'Probe') { + if (expected.Path !== undefined && !samePath(profile.path, expected.Path)) { + invalid('storage profile path'); + } + return result; + } if (result.operation !== operation) invalid('operation'); if (operation === 'FlushDirectory') { + if (expected.Path !== undefined && !samePath(profile.path, expected.Path)) { + invalid('directory flush path'); + } if (result.flushed !== true) invalid('directory flush'); } else if (operation === 'Rename') { + if (expected.From !== undefined && !sameRenamedSource(result.source, expected.From)) { + invalid('rename source path'); + } + if (expected.To !== undefined && !samePath(result.destination, expected.To)) { + invalid('rename destination path'); + } + if (expected.To !== undefined && + !samePath(profile.path, path.win32.dirname(expected.To))) { + invalid('rename parent-directory flush path'); + } if (result.renamed !== true || result.parentFlushCompleted !== true) { invalid('rename or parent-directory flush'); } From d32125cbcc7aa7b91119d80224eb575d948684d9 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 13:01:49 +0200 Subject: [PATCH 044/177] test(v0.13 T04): port W73-25 negative helper path/parent evidence regression --- scripts/state-engine-v013.test.mjs | 56 ++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/scripts/state-engine-v013.test.mjs b/scripts/state-engine-v013.test.mjs index 3a62041c2..0545794ef 100644 --- a/scripts/state-engine-v013.test.mjs +++ b/scripts/state-engine-v013.test.mjs @@ -559,6 +559,62 @@ test('W73-22 Windows native helper must prove operation-specific success before /helper|rename|profile|operation|flush/i); }); +test('W73-25 native Windows helper cannot acknowledge an unrelated filesystem path', () => { + const profile = { + path: 'C:\\dk\\parent', volumeRoot: 'C:\\', fileSystem: 'NTFS', + driveType: 3, localFixed: true, supportedAutomaticRestore: true, + }; + assert.deepEqual( + validateWindowsDurabilityHelperResult('Probe', profile, { Path: profile.path }), + profile, + ); + assert.throws( + () => validateWindowsDurabilityHelperResult('Probe', { + ...profile, path: 'C:\\dk\\unrelated', + }, { Path: profile.path }), + /helper|path|profile|mismatch/i, + 'A supported NTFS profile for a different path is not evidence for the requested root', + ); + + const flush = { operation: 'FlushDirectory', profile, flushed: true }; + assert.deepEqual( + validateWindowsDurabilityHelperResult('FlushDirectory', flush, { Path: profile.path }), + flush, + ); + assert.throws( + () => validateWindowsDurabilityHelperResult('FlushDirectory', { + ...flush, profile: { ...profile, path: 'C:\\dk\\unrelated' }, + }, { Path: profile.path }), + /helper|path|profile|mismatch/i, + 'A flush of a different parent cannot acknowledge the requested namespace barrier', + ); + + const rename = { + operation: 'Rename', source: 'C:\\dk\\stage', destination: 'C:\\dk\\project', + profile: { ...profile, path: 'C:\\dk' }, renamed: true, parentFlushCompleted: true, + }; + assert.deepEqual( + validateWindowsDurabilityHelperResult('Rename', rename, { + From: rename.source, To: rename.destination, + }), + rename, + ); + for (const wrong of [ + { ...rename, source: 'C:\\dk\\unrelated' }, + { ...rename, source: 'C:\\other-parent\\stage' }, + { ...rename, destination: 'C:\\dk\\unrelated' }, + { ...rename, profile: { ...profile, path: 'C:\\dk\\unrelated' } }, + ]) { + assert.throws( + () => validateWindowsDurabilityHelperResult('Rename', wrong, { + From: rename.source, To: rename.destination, + }), + /helper|path|profile|mismatch/i, + 'An unrelated rename or parent flush cannot acknowledge the requested transaction', + ); + } +}); + test('W73-21 Windows restore stage and promoted root preserve the original restrictive NTFS DACL', (t) => { if (process.platform !== 'win32') { t.skip('NTFS ACL fixture requires Windows'); return; From 993ec2a73158a472606a30afb5f0bc100a2b32b9 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 13:02:04 +0200 Subject: [PATCH 045/177] docs(v0.13): record W73-25 red/green CI provenance and remaining T04 acceptance gates --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index b67239f3f..ba9f775f5 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **W73-25 native-helper path binding (30 October 2026):** Draft diagnostic PR #105 reproduced an unbound `Probe` path acknowledgement on exact test-only source in Ubuntu CI #41042850430 (RED, missing expected exception). The initial correction exposed genuine Windows 8.3 ancestor-alias and retained V1 journal-file rename compatibility failures in CI #41042984647 and #41043503806; these were corrected using live parent-directory identity for a renamed source and exact file/directory inode identity for published destinations. Final diagnostic commit `49c9abf0de43d50c2ca8748c2c173e51f0353275` passed the complete Ubuntu and Windows v0.13 validation lanes in CI #41044118361. Only the validated runtime and W73-25 regression test were ported to actual draft integration PR #92 as commits `6b89d9dba299b05c438480c647038590ba2d27e9` and `d32125cbcc7aa7b91119d80224eb575d948684d9`; diagnostic workflow exceptions were not ported. The helper now rejects successful responses for a different requested storage-profile, flush, source, destination or parent-flush path. This is API/path-binding evidence, **not physical NTFS power-loss proof**. Exact-head PR #92 CI, distinct independent role reviews and persisted deterministic risk-3 `ACCEPTED` remain outstanding. Issue #73 remains OPEN. + - **30 October exact-source verification checkpoint:** integration PR #92 head `1f2c909816287aeceb7aca1f2fd4e6dd9a68aadf` passed GitHub Actions CI #41030766378 on **both Ubuntu and Windows** (two platform jobs in one workflow execution; NOT two independent full runs). The diagnostic PR #99 head `51556f841bb07e9ed27436e9156bfff022487de1` passed both platform jobs in CI #41015133002; the diagnostic head is divergent and must not be merged as a substitute for PR #92. W73-21/23/24 NTFS ACL and helper-identity corrections are integrated on PR #92. Current acceptance blockers remain **controlled NTFS physical power-loss evidence under Issue #73, distinct authenticated exact-source code/architecture/security reviews, repeated complete platform validation on the final reviewed SHA, and persisted deterministic risk-3 ACCEPTED**. No acceptance, release or merge is asserted. Updating this file creates a new source commit and therefore requires fresh CI and review binding. - **NTFS DACL private-stage hardening (30 October, W73-21/23/24):** diagnostic draft PR #104 reproduced missing ACL preservation and unverified native-helper responses on exact negative-control source (CI #41029403553). The v0.13 Windows helper now creates a maintenance-only protected stage at creation and retains its private root DACL throughout sensitive file copy; descendant original DACLs are restored post-order and whole-tree verified before the original root DACL is installed last. Native helper result checks bind operations to the actual directory identity (including verified NTFS 8.3 short-path aliases) and reject false/mismatched results. Focused real Windows NTFS W73-21/23/24 plus legacy restore fixtures passed with zero skips in CI #41029930153; Ubuntu full validation passed in CI #41029930369, with Windows full compatibility still pending at the time of this memory update. Ported only source/test files from diagnostic PR #104 into integration PR #92 through commit `11c2843bef8b8363fa39ff248ead38de705ebef0`; diagnostic workflow exceptions were NOT ported. **This is not NTFS physical power-loss proof or risk-3 ACCEPTED.** Issue #73 and three-role independent reviews remain blocking. From cd534ad03a1f140bf6aa15ea5ba73145321f56a7 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 13:45:08 +0200 Subject: [PATCH 046/177] fix(v0.13 T04): pin native Windows PowerShell helper to verified system path W73-26 --- .../orchestration/state-engine-migration.mjs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index 767e5a549..e36b13e81 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -1007,6 +1007,46 @@ export function validateWindowsDurabilityHelperResult(operation, result, expecte return result; } +/** + * Resolve the Windows inbox interpreter without libuv's cwd/PATH executable + * search. The SystemRoot process environment is an explicit trusted launcher + * precondition: absent, relative, inconsistent or redirected locations fail + * closed before any journal or restore state can be acknowledged. + */ +function trustedWindowsPowerShellExecutable() { + const systemRoot = process.env.SystemRoot; + const windowsDirectory = process.env.windir; + const invalidRoot = typeof systemRoot !== 'string' || + !/^[a-zA-Z]:\\/.test(systemRoot) || + systemRoot.split(/[\\/]/).some((part) => part === '..' || part === '.'); + if (invalidRoot || (windowsDirectory && + path.win32.normalize(windowsDirectory).toLowerCase() !== + path.win32.normalize(systemRoot).toLowerCase())) { + throw new StateMigrationError('Trusted Windows system directory is unavailable or inconsistent', { + code: 'TRUSTED_WINDOWS_INTERPRETER_UNAVAILABLE', + }); + } + + const executable = path.win32.join(systemRoot, + 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'); + try { + const rootReal = fs.realpathSync.native(systemRoot); + const exeReal = fs.realpathSync.native(executable); + const stat = fs.lstatSync(executable); + const expectedRelative = path.win32.join('System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'); + if (!stat.isFile() || stat.isSymbolicLink() || + path.win32.relative(rootReal, exeReal).toLowerCase() !== expectedRelative.toLowerCase()) { + throw new Error('System PowerShell executable is not a regular file at its expected location'); + } + return exeReal; + } catch (error) { + throw new StateMigrationError('Trusted Windows PowerShell interpreter cannot be verified', { + code: 'TRUSTED_WINDOWS_INTERPRETER_UNAVAILABLE', + cause: error.code || error.message, + }); + } +} + function runWindowsDurabilityHelper(operation, args = {}) { if (process.platform !== 'win32') { throw new StateMigrationError('Windows durability helper may only run on Windows'); @@ -1031,7 +1071,9 @@ function runWindowsDurabilityHelper(operation, args = {}) { argv.push('-' + key, String(value)); } - const result = spawnSync('powershell.exe', argv, { + const interpreter = trustedWindowsPowerShellExecutable(); + const result = spawnSync(interpreter, argv, { + cwd: path.win32.dirname(interpreter), encoding: 'utf8', windowsHide: true, shell: false, From ef55106668ecec7070f326275c3024636881b7ac Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 13:45:18 +0200 Subject: [PATCH 047/177] test(v0.13 T04): prevent Windows cwd PowerShell executable hijack W73-26 --- scripts/state-engine-v2-migration.test.mjs | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index a9efed55e..e4f8ac242 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -1047,3 +1047,32 @@ test('T04-H39 backup with trailing dot or space path alias is refused before res ); assert.deepEqual(fs.readdirSync(targetRoot), []); }); + + +test('W73-26 Windows helper launch ignores a forged powershell.exe in the working directory', (t) => { + if (process.platform !== 'win32') { + t.skip('Windows executable-search hijack regression requires Windows'); return; + } + const source = tempProject(t, 'dk-win-helper-launch-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const hostileWorkingDir = tempProject(t, 'dk-win-helper-launch-cwd-'); + // A copy of Node named powershell.exe is an executable canary: invoking it + // with PowerShell flags fails. The restore must instead use the trusted + // system interpreter by absolute path, never libuv's cwd-first lookup. + fs.copyFileSync(process.execPath, path.join(hostileWorkingDir, 'powershell.exe')); + const targetRoot = path.join(hostileWorkingDir, 'restored-project'); + const script = [ + 'import { restoreLegacyBackup } from ' + + JSON.stringify(new URL('../runtime/orchestration/state-engine-migration.mjs', import.meta.url).href) + ';', + 'restoreLegacyBackup({ backupPath: process.argv[1], targetRoot: process.argv[2], confirmOffline: true });', + ].join('\n'); + const child = requireSpawnSync(process.execPath, + ['--input-type=module', '-e', script, migrated.backupPath, targetRoot], { + cwd: hostileWorkingDir, encoding: 'utf8', timeout: 120_000, windowsHide: true, + }); + assert.equal(child.status, 0, + 'Windows restore must not execute cwd/powershell.exe: ' + (child.stderr || child.stdout)); + assert.equal(fs.existsSync(path.join(targetRoot, '.development-kit', 'autopilot', 'state', 'current.json')), true, + 'A trusted helper launch must complete the original legacy backup restoration'); +}); From f372d2ad52f1446f978be83fa19e50b3a999ba94 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 14:18:28 +0200 Subject: [PATCH 048/177] fix(v0.13 T04): reject restore journals across canonical NTFS ancestor aliases (W73-27) --- .../orchestration/state-engine-migration.mjs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index e36b13e81..fea7b1b5d 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -803,13 +803,31 @@ function orphanRestoreWitnesses(targetRoot) { * This is a fail-closed alias guard, not proof of physical power-loss * durability or a defence against a hostile same-account filesystem writer. */ +/** + * Compare the physical Windows target identity, not caller-supplied spelling. + * GetFinalPathNameByHandle (via realpathSync.native) resolves existing NTFS + * 8.3 ancestor aliases and extended-length namespace spellings. When the + * root was displaced by a crash, canonicalize its surviving parent and + * preserve the leaf name. This is a journal-discovery guard, not a guarantee + * of persistence after sudden power loss. + */ +function windowsCanonicalRestoreRootKey(targetRoot) { + const root = path.resolve(targetRoot); + const parent = path.dirname(root); + const canonicalParent = fs.realpathSync.native(parent); + const canonicalRoot = fs.existsSync(root) + ? fs.realpathSync.native(root) + : path.win32.join(canonicalParent, path.basename(root)); + return path.win32.normalize(canonicalRoot).normalize('NFC').toLowerCase(); +} + function assertNoWindowsCaseAliasedRestoreJournal(targetRoot) { if (process.platform !== 'win32') return; const root = path.resolve(targetRoot); const parent = path.dirname(root); if (!fs.existsSync(parent)) return; const expectedJournal = path.basename(restoreJournalPath(root)).toLowerCase(); - const targetKey = root.normalize('NFC').toLowerCase(); + const targetKey = windowsCanonicalRestoreRootKey(root); for (const name of fs.readdirSync(parent)) { if (!/^\.dk-restore-transaction-[a-f0-9]{24}\.jsonl$/i.test(name) || name.toLowerCase() === expectedJournal) continue; @@ -829,7 +847,7 @@ function assertNoWindowsCaseAliasedRestoreJournal(targetRoot) { try { witness = JSON.parse(firstLine); } catch { continue; } if (typeof witness?.targetRoot !== 'string' || - path.resolve(witness.targetRoot).normalize('NFC').toLowerCase() !== targetKey) continue; + windowsCanonicalRestoreRootKey(witness.targetRoot) !== targetKey) continue; validateRestoreRecord(witness); throw new StateMigrationError( 'Terminal restore root witness exists under a Windows case alias; refusing destructive mutation', From d78a98e7ac336bf1c5b2e22cd1ad2a9f211bec4a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 14:18:31 +0200 Subject: [PATCH 049/177] test(v0.13 T04): verify interrupted restore journal blocks alternate Windows ancestor spelling --- scripts/state-engine-v2-migration.test.mjs | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index e4f8ac242..4baec1569 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -1076,3 +1076,65 @@ test('W73-26 Windows helper launch ignores a forged powershell.exe in the workin assert.equal(fs.existsSync(path.join(targetRoot, '.development-kit', 'autopilot', 'state', 'current.json')), true, 'A trusted helper launch must complete the original legacy backup restoration'); }); + +test('W73-27 Windows 8.3 ancestor alias cannot bypass an interrupted restore journal', (t) => { + if (process.platform !== 'win32') { + t.skip('NTFS short-name aliases require Windows'); return; + } + const source = tempProject(t, 'dk-short-alias-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const parent = tempProject(t, 'dk-short-alias-parent-'); + const targetRoot = path.join(parent, 'target'); + fs.mkdirSync(targetRoot); + fs.writeFileSync(path.join(targetRoot, 'original.txt'), 'KEEP-ORIGINAL'); + const command = 'for %I in ("' + parent + '") do @echo %~sI'; + const shortResult = requireSpawnSync(process.env.ComSpec || 'cmd.exe', + ['/d', '/c', command], { encoding: 'utf8', windowsHide: true }); + assert.equal(shortResult.status, 0, shortResult.stderr); + const shortParent = shortResult.stdout.trim().split(/\r?\n/).at(-1); + let aliasParent = shortParent; + if (!fs.existsSync(shortParent) || + path.win32.normalize(shortParent).toLowerCase() === path.win32.normalize(parent).toLowerCase()) { + // The extended-length namespace is a second real spelling of the same + // Windows directory even when NTFS 8.3 name generation is disabled. + const extendedParent = '\\\\?\\' + parent; + if (!fs.existsSync(extendedParent)) { + t.skip('Neither an 8.3 nor an extended-length ancestor alias is available'); return; + } + aliasParent = extendedParent; + } + assert.equal(fs.realpathSync.native(aliasParent).toLowerCase(), + fs.realpathSync.native(parent).toLowerCase(), 'Alias must identify the same physical parent'); + const crashScript = [ + 'import fs from "node:fs";', + 'import {restoreLegacyBackup} from ' + + JSON.stringify(new URL('../runtime/orchestration/state-engine-migration.mjs', import.meta.url).href) + ';', + 'const write=fs.writeFileSync;', + 'fs.writeFileSync=function(fd, content, ...rest) {', + ' const result=write.call(this, fd, content, ...rest);', + ' if(typeof fd==="number" && String(content).includes(\'"phase":"PREPARED"\')) process.exit(91);', + ' return result;', + '};', + 'restoreLegacyBackup({backupPath:process.argv[1],targetRoot:process.argv[2],confirmOffline:true});', + ].join('\n'); + const crashed = requireSpawnSync(process.execPath, + ['--input-type=module', '-e', crashScript, migrated.backupPath, targetRoot], + { encoding: 'utf8', windowsHide: true, timeout: 120_000 }); + assert.equal(crashed.status, 91, crashed.stderr || crashed.stdout); + const journals = fs.readdirSync(parent).filter(name => + /^\.dk-restore-transaction-[a-f0-9]{24}\.jsonl$/i.test(name)); + assert.equal(journals.length, 1, 'Interrupted transaction must retain its journal'); + const journalBytes = fs.readFileSync(path.join(parent, journals[0])); + const shortRoot = path.join(aliasParent, 'target'); + assert.throws( + () => restoreLegacyBackup({ + backupPath: migrated.backupPath, targetRoot: shortRoot, + overwrite: true, confirmOffline: true, + }), + /alias|journal|incomplete|witness/i, + 'Same physical target through 8.3 ancestor must not start a second transaction', + ); + assert.deepEqual(fs.readFileSync(path.join(targetRoot, 'original.txt'), 'utf8'), 'KEEP-ORIGINAL'); + assert.deepEqual(fs.readFileSync(path.join(parent, journals[0])), journalBytes); +}); From 7351c08c2aae444c3230b1397c7a805ccfde9b73 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 14:18:42 +0200 Subject: [PATCH 050/177] docs(v0.13 T04): record test-first W73-27 Windows alias evidence and limitations --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index ba9f775f5..0fd18f85f 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **W73-27 Windows ancestor alias journal-discovery hardening (30 October 2026):** diagnostic draft PR #108, test-first. Real Windows NTFS 8.3 short names were unavailable on the hosted runner (initial CI #41051765992 reported an explicit skip), so the test was extended to the alternate Windows extended-length namespace spelling of the *same verified physical parent*. Test-only CI #41052720876 reproduced a missing alias-journal rejection on Windows (the second restore reached unsafe-path preflight and failed with `EISDIR` instead of identifying the retained interrupted journal); Ubuntu passed. The corrected guard compares canonical physical parent/root identity using `fs.realpathSync.native`, including a missing target root after displacement, before allowing a new transaction. The diagnostic correction passed both complete Ubuntu and Windows v0.13 compatibility/regression jobs in [CI #41053285824](https://github.com/eybersjp/development-kit/actions/runs/41053285824). Only source and W73-27 regression test were ported to integration PR #92 in commits `f372d2ad52f1446f978be83fa19e50b3a999ba94` and `d78a98e7ac336bf1c5b2e22cd1ad2a9f211bec4a`; diagnostic CI workflow was **not** ported. **Limits:** extended-length alias behavior was exercised, not an actual NTFS 8.3 alias on this host; no physical sudden-power-loss durability is established, and this does not resolve Issue #73 or constitute independent risk-3 acceptance. Exact integration-head CI and independent source-bound reviews remain mandatory. + - **W73-25 native-helper path binding (30 October 2026):** Draft diagnostic PR #105 reproduced an unbound `Probe` path acknowledgement on exact test-only source in Ubuntu CI #41042850430 (RED, missing expected exception). The initial correction exposed genuine Windows 8.3 ancestor-alias and retained V1 journal-file rename compatibility failures in CI #41042984647 and #41043503806; these were corrected using live parent-directory identity for a renamed source and exact file/directory inode identity for published destinations. Final diagnostic commit `49c9abf0de43d50c2ca8748c2c173e51f0353275` passed the complete Ubuntu and Windows v0.13 validation lanes in CI #41044118361. Only the validated runtime and W73-25 regression test were ported to actual draft integration PR #92 as commits `6b89d9dba299b05c438480c647038590ba2d27e9` and `d32125cbcc7aa7b91119d80224eb575d948684d9`; diagnostic workflow exceptions were not ported. The helper now rejects successful responses for a different requested storage-profile, flush, source, destination or parent-flush path. This is API/path-binding evidence, **not physical NTFS power-loss proof**. Exact-head PR #92 CI, distinct independent role reviews and persisted deterministic risk-3 `ACCEPTED` remain outstanding. Issue #73 remains OPEN. - **30 October exact-source verification checkpoint:** integration PR #92 head `1f2c909816287aeceb7aca1f2fd4e6dd9a68aadf` passed GitHub Actions CI #41030766378 on **both Ubuntu and Windows** (two platform jobs in one workflow execution; NOT two independent full runs). The diagnostic PR #99 head `51556f841bb07e9ed27436e9156bfff022487de1` passed both platform jobs in CI #41015133002; the diagnostic head is divergent and must not be merged as a substitute for PR #92. W73-21/23/24 NTFS ACL and helper-identity corrections are integrated on PR #92. Current acceptance blockers remain **controlled NTFS physical power-loss evidence under Issue #73, distinct authenticated exact-source code/architecture/security reviews, repeated complete platform validation on the final reviewed SHA, and persisted deterministic risk-3 ACCEPTED**. No acceptance, release or merge is asserted. Updating this file creates a new source commit and therefore requires fresh CI and review binding. From e92c2fd1df681751c2ff70e565ba5c5e1beaf731 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 14:49:21 +0200 Subject: [PATCH 051/177] fix(t04): port verified prototype-safe patch traversal from diagnostic PR #109 --- runtime/orchestration/state-engine-v2.mjs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index c9457ca1d..72e5ca34f 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -765,9 +765,9 @@ function diffValues(before, after, pathSegments = []) { const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort(); for (const key of keys) { const childPath = [...pathSegments, key]; - if (!(key in after)) { + if (!Object.hasOwn(after, key)) { operations.push({ op: 'delete', path: childPath }); - } else if (!(key in before)) { + } else if (!Object.hasOwn(before, key)) { operations.push({ op: 'set', path: childPath, value: cloneSerializable(after[key], 'Patch value') }); } else { operations.push(...diffValues(before[key], after[key], childPath)); @@ -779,15 +779,25 @@ function diffValues(before, after, pathSegments = []) { return [{ op: 'set', path: pathSegments, value: cloneSerializable(after, 'Patch value') }]; } +function defineOwnPatchValue(target, key, value) { + // Literal JSON keys such as "__proto__" must remain own data properties. + // Assignment can invoke inherited setters and mutate a process prototype. + Object.defineProperty(target, key, { + value, enumerable: true, writable: true, configurable: true, + }); +} + function setAtPath(target, segments, value) { if (segments.length === 0) return cloneSerializable(value, 'Patch root value'); let cursor = target; for (let i = 0; i < segments.length - 1; i += 1) { const key = segments[i]; - if (!plainObject(cursor[key])) cursor[key] = {}; + if (!Object.hasOwn(cursor, key) || !plainObject(cursor[key])) { + defineOwnPatchValue(cursor, key, {}); + } cursor = cursor[key]; } - cursor[segments.at(-1)] = cloneSerializable(value, 'Patch value'); + defineOwnPatchValue(cursor, segments.at(-1), cloneSerializable(value, 'Patch value')); return target; } @@ -796,10 +806,10 @@ function deleteAtPath(target, segments) { let cursor = target; for (let i = 0; i < segments.length - 1; i += 1) { const key = segments[i]; - if (!plainObject(cursor[key])) return target; + if (!Object.hasOwn(cursor, key) || !plainObject(cursor[key])) return target; cursor = cursor[key]; } - delete cursor[segments.at(-1)]; + if (Object.hasOwn(cursor, segments.at(-1))) delete cursor[segments.at(-1)]; return target; } From 42df7f1f58ba899e817618964a5975b5f8ace52f Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 14:49:27 +0200 Subject: [PATCH 052/177] test(t04): port reproduced prototype-pollution regression with cross-platform CI #41057264710 --- scripts/state-engine-v2.test.mjs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scripts/state-engine-v2.test.mjs b/scripts/state-engine-v2.test.mjs index 9bda942d0..2bb98dedb 100644 --- a/scripts/state-engine-v2.test.mjs +++ b/scripts/state-engine-v2.test.mjs @@ -370,3 +370,32 @@ test('orchestration V2 refuses same-revision semantic overwrite', (t) => { /Refusing to overwrite orchestration run state revision/, ); }); + +test('T04-P1 patch replay preserves own __proto__ data without prototype pollution', (t) => { + const rootDir = tempProject(t, 'dk-t04-patch-proto-'); + const key = '__t04PatchPollutionProbe'; + assert.equal(Object.hasOwn(Object.prototype, key), false, 'probe must start clean'); + try { + appendEntityState({ + rootDir, entityType: 'generic-state', entityId: 'prototype-safety', + state: { stateRevision: 1, metadata: {} }, actorClass: 'security-regression', + }); + const changed = JSON.parse('{"stateRevision":2,"metadata":{"__proto__":{"__t04PatchPollutionProbe":true},"safe":42}}'); + appendEntityState({ + rootDir, entityType: 'generic-state', entityId: 'prototype-safety', + state: changed, actorClass: 'security-regression', + }); + assert.equal(Object.hasOwn(Object.prototype, key), false, + 'patch traversal must never mutate Object.prototype'); + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); + const actual = snapshot.entities['generic-state']['prototype-safety'].state; + assert.equal(Object.hasOwn(actual.metadata, '__proto__'), true, + 'literal JSON __proto__ must be retained as an own data property'); + assert.deepEqual(actual.metadata, changed.metadata); + const replay = rebuildSnapshotFromEvents(loadCanonicalEvents(rootDir)); + assert.deepEqual(replay.entities['generic-state']['prototype-safety'].state, changed); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); + } finally { + delete Object.prototype[key]; + } +}); From 99fe01dd428f48bb7b4499213dbadde27b028f61 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 14:52:33 +0200 Subject: [PATCH 053/177] fix(t04): port verified exclusive initial ledger publication from diagnostic PR #110 --- runtime/orchestration/state-engine-v2.mjs | 69 +++++++++++++++++++---- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 72e5ca34f..0744ae60d 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -496,30 +496,77 @@ function schemaDocument() { }); } +/** + * Create an initial layout entry without ever replacing a concurrently + * published canonical file. Atomic rename of an empty bootstrap ledger is + * unsafe: an independent writer may commit between existsSync and rename. + * O_EXCL ('wx') makes the first creator win; an existing entry is never + * overwritten. Directory identity and durability checks match atomicWrite. + */ +function createInitialStateFile(filePath, content) { + const directory = path.dirname(filePath); + const dirFd = fs.openSync(directory, 'r'); + try { + const pinned = fs.fstatSync(dirFd); + const observed = fs.lstatSync(directory); + if (!pinned.isDirectory() || !observed.isDirectory() || observed.isSymbolicLink() || + pinned.dev !== observed.dev || pinned.ino !== observed.ino) { + throw new StateEngineError('State Engine directory changed before exclusive initialization'); + } + const root = path.dirname(path.dirname(directory)); + assertStateRootRealpathSafe({ root, stateRoot: directory }); + const destination = process.platform === 'linux' + ? path.join('/proc/self/fd/' + dirFd, path.basename(filePath)) + : filePath; + let fd; + try { + fd = fs.openSync(destination, 'wx', 0o600); + } catch (error) { + if (error.code === 'EEXIST') return false; + throw error; + } + try { + fs.writeFileSync(fd, content, 'utf8'); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + const latest = fs.lstatSync(directory); + if (!latest.isDirectory() || latest.isSymbolicLink() || + latest.dev !== pinned.dev || latest.ino !== pinned.ino) { + throw new StateEngineError('State Engine directory changed during exclusive initialization'); + } + flushStateDirectory(directory, dirFd); + return true; + } finally { + fs.closeSync(dirFd); + } +} + export function ensureStateEngineLayout(rootDir = process.cwd()) { const paths = statePaths(rootDir); fs.mkdirSync(paths.stateRoot, { recursive: true }); assertStateRootRealpathSafe(paths); const expectedSchema = stablePretty(schemaDocument()); - if (fs.existsSync(paths.schema)) { - const existing = fs.readFileSync(paths.schema, 'utf8'); - if (existing !== expectedSchema) { - throw new StateEngineError('State Engine schema.json does not match the runtime schema contract'); - } - } else { - atomicWrite(paths.schema, expectedSchema); + if (!fs.existsSync(paths.schema)) createInitialStateFile(paths.schema, expectedSchema); + if (fs.readFileSync(paths.schema, 'utf8') !== expectedSchema) { + throw new StateEngineError('State Engine schema.json does not match the runtime schema contract'); } if (!fs.existsSync(paths.events)) { if (fs.existsSync(paths.pending)) { throw new StateEngineError('State Engine events.jsonl is missing while pending-commit.json is present; state integrity compromised'); } - atomicWrite(paths.events, ''); + createInitialStateFile(paths.events, ''); } - if (!fs.existsSync(paths.snapshot)) atomicWrite(paths.snapshot, stablePretty(emptySnapshot())); - if (!fs.existsSync(paths.index)) atomicWrite(paths.index, stablePretty(buildStateIndex(emptySnapshot()))); - + if (!fs.existsSync(paths.snapshot)) { + createInitialStateFile(paths.snapshot, stablePretty(emptySnapshot())); + } + if (!fs.existsSync(paths.index)) { + createInitialStateFile(paths.index, stablePretty(buildStateIndex(emptySnapshot()))); + } + assertStateFilesNotLinks(paths); return paths; } From f9fde7c4ea56c1bd2be1a373c0ad7f9fcbb4337c Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 14:52:44 +0200 Subject: [PATCH 054/177] test(t04): port reproduced first-layout writer race with Ubuntu/Windows CI #41057594721 --- scripts/state-engine-v2-hardening.test.mjs | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index a15e9ed9b..8e3b41571 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -1046,3 +1046,39 @@ test('T04-H50 Windows flushes pending-journal directory before any physical cano assert.equal(fs.existsSync(paths.pending), false); assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); }); + +test('T04-P1 initial layout cannot overwrite an acknowledged concurrent first event', (t) => { + const rootDir = tempProject(t); + const paths = getStateEnginePaths(rootDir); + const originalRename = fs.renameSync; + const originalOpen = fs.openSync; + let injected = false; + const injectWriter = () => { + injected = true; + appendMetadataEvent({ + rootDir, eventType: 'RACE_COMMIT', actorClass: 'concurrency-regression', + payload: { witness: 'acknowledged-before-initial-layout' }, + }); + }; + fs.renameSync = function raceOnFirstLedgerPublication(from, to, ...rest) { + if (!injected && path.basename(String(to)) === 'events.jsonl' && String(from).includes('events.jsonl.tmp-')) { + injectWriter(); + } + return originalRename.call(this, from, to, ...rest); + }; + fs.openSync = function raceOnExclusiveLedgerCreation(file, flags, ...rest) { + if (!injected && path.basename(String(file)) === 'events.jsonl' && flags === 'wx') injectWriter(); + return originalOpen.call(this, file, flags, ...rest); + }; + try { + ensureStateEngineLayout(rootDir); + } finally { + fs.renameSync = originalRename; + fs.openSync = originalOpen; + } + assert.equal(injected, true, 'fixture must interleave first layout with a successful writer'); + const ledger = fs.readFileSync(paths.events, 'utf8'); + assert.match(ledger, /RACE_COMMIT/, 'initialization must not replace an acknowledged canonical event'); + assert.equal(loadCanonicalEvents(rootDir).length, 1); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From 3dd4f0aac2fb2ea9cd63b1e99419211220ec3d41 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 14:52:57 +0200 Subject: [PATCH 055/177] docs(t04): record test-first P1 evidence, exact integration ports and remaining acceptance boundaries --- memory.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/memory.md b/memory.md index 0fd18f85f..796ca2d51 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,11 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-P1 prototype-safe patch replay (30 October 2026):** actual integration PR #92 independent review found inherited-property traversal in `diffValues`/`setAtPath`/`deleteAtPath`. Isolated diagnostic draft PR #109 test-only commit `692084d0b02194a39f563e1f90705077eb771f75` reproduced process-wide `Object.prototype` pollution in Ubuntu/Windows CI [#41057142898](https://github.com/eybersjp/development-kit/actions/runs/41057142898). Correction `7d55832a640400948d7cf896bcc18b822c223471` uses `Object.hasOwn` traversal and `Object.defineProperty` for literal JSON keys; both full v0.13 Ubuntu and Windows lanes passed [#41057264710](https://github.com/eybersjp/development-kit/actions/runs/41057264710). Only source/test changes were ported to PR #92 in commits `e92c2fd1df681751c2ff70e565ba5c5e1beaf731` and `42df7f1f58ba899e817618964a5975b5f8ace52f`; diagnostic CI exception was not ported. +- **T04-P1 first-layout concurrent ledger loss (30 October 2026):** PR #92 review found `ensureStateEngineLayout` could rename an empty initial ledger over an already acknowledged concurrent writer. Isolated diagnostic draft PR #110 corrected its deterministic Linux descriptor-relative interleaving fixture at `079a92a9a8b8d9b379a7b774d5edcc2790c91d74`; test-only Ubuntu/Windows CI [#41057459611](https://github.com/eybersjp/development-kit/actions/runs/41057459611) reproduced loss of the acknowledged event (empty `events.jsonl`). Correction `d1b1d9e32e1ac74e4b8ee98fd7a1fdb7c6a79915` creates initial layout entries exclusively with pinned-directory identity and fsync barriers, never replacing an existing writer-created ledger. Both full Ubuntu and Windows v0.13 validation lanes passed [#41057594721](https://github.com/eybersjp/development-kit/actions/runs/41057594721). Only source/test changes were ported to PR #92 in commits `99fe01dd428f48bb7b4499213dbadde27b028f61` and `f9fde7c4ea56c1bd2be1a373c0ad7f9fcbb4337c`; diagnostic CI exception was not ported. +- **Acceptance boundary:** the two diagnostic green runs validate separate source corrections, not final combined source acceptance. PR #92 must pass fresh exact-head full Ubuntu/Windows runs and independent distinct code/architecture/security review after this memory update; Issue #73 still requires controlled physical NTFS sudden-power-loss evidence. Do not merge, release or persist `ACCEPTED` without every risk-3 gate. + + - **W73-27 Windows ancestor alias journal-discovery hardening (30 October 2026):** diagnostic draft PR #108, test-first. Real Windows NTFS 8.3 short names were unavailable on the hosted runner (initial CI #41051765992 reported an explicit skip), so the test was extended to the alternate Windows extended-length namespace spelling of the *same verified physical parent*. Test-only CI #41052720876 reproduced a missing alias-journal rejection on Windows (the second restore reached unsafe-path preflight and failed with `EISDIR` instead of identifying the retained interrupted journal); Ubuntu passed. The corrected guard compares canonical physical parent/root identity using `fs.realpathSync.native`, including a missing target root after displacement, before allowing a new transaction. The diagnostic correction passed both complete Ubuntu and Windows v0.13 compatibility/regression jobs in [CI #41053285824](https://github.com/eybersjp/development-kit/actions/runs/41053285824). Only source and W73-27 regression test were ported to integration PR #92 in commits `f372d2ad52f1446f978be83fa19e50b3a999ba94` and `d78a98e7ac336bf1c5b2e22cd1ad2a9f211bec4a`; diagnostic CI workflow was **not** ported. **Limits:** extended-length alias behavior was exercised, not an actual NTFS 8.3 alias on this host; no physical sudden-power-loss durability is established, and this does not resolve Issue #73 or constitute independent risk-3 acceptance. Exact integration-head CI and independent source-bound reviews remain mandatory. - **W73-25 native-helper path binding (30 October 2026):** Draft diagnostic PR #105 reproduced an unbound `Probe` path acknowledgement on exact test-only source in Ubuntu CI #41042850430 (RED, missing expected exception). The initial correction exposed genuine Windows 8.3 ancestor-alias and retained V1 journal-file rename compatibility failures in CI #41042984647 and #41043503806; these were corrected using live parent-directory identity for a renamed source and exact file/directory inode identity for published destinations. Final diagnostic commit `49c9abf0de43d50c2ca8748c2c173e51f0353275` passed the complete Ubuntu and Windows v0.13 validation lanes in CI #41044118361. Only the validated runtime and W73-25 regression test were ported to actual draft integration PR #92 as commits `6b89d9dba299b05c438480c647038590ba2d27e9` and `d32125cbcc7aa7b91119d80224eb575d948684d9`; diagnostic workflow exceptions were not ported. The helper now rejects successful responses for a different requested storage-profile, flush, source, destination or parent-flush path. This is API/path-binding evidence, **not physical NTFS power-loss proof**. Exact-head PR #92 CI, distinct independent role reviews and persisted deterministic risk-3 `ACCEPTED` remain outstanding. Issue #73 remains OPEN. From fb25ff00fb3306f3ff5990c85c771291af12d328 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 15:43:03 +0200 Subject: [PATCH 056/177] test(t04): enforce v0.13 CI source routing and retained risk-3 contract --- scripts/v071-regression.test.mjs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/v071-regression.test.mjs b/scripts/v071-regression.test.mjs index bbf1d2858..717ae22ea 100644 --- a/scripts/v071-regression.test.mjs +++ b/scripts/v071-regression.test.mjs @@ -380,3 +380,24 @@ test('TEST M: remembered approval still cannot authorize /dk-ship or any consequ assert.equal(shipMeta.requiresApproval, true); assert.equal(shipMeta.safetyLevel, 'consequential'); }); + + +test('V13-CI-ROUTING: v0.13 validation is selected by source presence, not mutable PR branch name', (t) => { + // The published v0.12.1 line does not contain this contract. Its CI must + // continue running the v0.12 release scope without enabling deferred T04. + const engine = join(REPO_ROOT, 'runtime', 'orchestration', 'state-engine-v2.mjs'); + if (!existsSync(engine)) { + t.skip('v0.13 T04 runtime is absent from the released baseline'); + return; + } + assert.ok(existsSync(join(REPO_ROOT, 'scripts', 'v013-state-engine-v2-contract.mjs')), + 'Present v0.13 runtime must retain its required risk-3 Development Contract'); + const workflow = readFileSync(join(REPO_ROOT, '.github', 'workflows', 'ci.yml'), 'utf8'); + const sourceProbe = String.raw`hashFiles\('runtime/orchestration/state-engine-v2\.mjs'\)`; + const positive = new RegExp(String.raw`if:\s*\$\{\{\s*${sourceProbe}\s*!=\s*''\s*\}\}`); + const negative = new RegExp(String.raw`if:\s*\$\{\{\s*${sourceProbe}\s*==\s*''\s*\}\}`); + assert.doesNotMatch(workflow, /github\.head_ref/, 'Branch-name conditions can silently skip T04 validation after a merge or branch rename'); + assert.match(workflow, positive, 'v0.13 test lane must run whenever its canonical runtime is in the checkout'); + assert.match(workflow, negative, 'v0.12-only release lane must run only without the v0.13 runtime'); + assert.match(workflow, /run: npm run v013:regression/, 'Source-bound v0.13 lane must include full compatibility regression'); +}); From a9d114f17ccb56be1795024f7b2cfa943d34b686 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 15:43:10 +0200 Subject: [PATCH 057/177] fix(t04): enforce v0.13 CI by source presence across PRs and main pushes --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 446d1f242..c72b44f82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,11 +99,11 @@ jobs: run: npm run execution-capsule:test - name: Validate v0.12 reduced-release scope - if: ${{ github.head_ref != 'feature/v0.13-state-engine-v2' }} + if: ${{ hashFiles('runtime/orchestration/state-engine-v2.mjs') == '' }} run: npm run release-scope:v012 - name: Validate v0.13 State Engine V2 increment - if: ${{ github.head_ref == 'feature/v0.13-state-engine-v2' }} + if: ${{ hashFiles('runtime/orchestration/state-engine-v2.mjs') != '' }} run: npm run v013-state-engine-v2:test - name: Validate v0.12 non-T04 feature increments @@ -125,13 +125,13 @@ jobs: run: npm pack --dry-run - name: Verify installed v0.12 consumer package - if: ${{ github.head_ref != 'feature/v0.13-state-engine-v2' }} + if: ${{ hashFiles('runtime/orchestration/state-engine-v2.mjs') == '' }} run: npm run consumer-smoke:v012 - name: Validate exact release command - if: ${{ github.head_ref != 'feature/v0.13-state-engine-v2' }} + if: ${{ hashFiles('runtime/orchestration/state-engine-v2.mjs') == '' }} run: npm run release:validate - name: Validate v0.13 full compatibility regression - if: ${{ github.head_ref == 'feature/v0.13-state-engine-v2' }} + if: ${{ hashFiles('runtime/orchestration/state-engine-v2.mjs') != '' }} run: npm run v013:regression From 81e37539f25472f8db0cf4ee33f242132f38dd0d Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 15:43:25 +0200 Subject: [PATCH 058/177] docs(t04): record test-first v0.13 CI routing evidence and acceptance limits --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index 796ca2d51..c84298073 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-CI-01 source-bound v0.13 CI routing (30 October 2026):** actual integration PR #92 previously selected v0.13 full compatibility regression only when `github.head_ref` exactly equalled `feature/v0.13-state-engine-v2`. On a `main` push or renamed PR branch this condition could skip T04 validation despite the canonical runtime being present. Isolated diagnostic draft [PR #111](https://github.com/eybersjp/development-kit/pull/111) reproduced the branch-name bypass with a test-only regression in Ubuntu [CI #41062682013](https://github.com/eybersjp/development-kit/actions/runs/41062682013) (RED). A first source-bound correction passed Ubuntu/Windows [CI #41062788215](https://github.com/eybersjp/development-kit/actions/runs/41062788215) (GREEN), but the follow-on adversarial test proved that keying on the *contract file* alone allowed deletion of that contract to bypass T04 validation; [CI #41063850666](https://github.com/eybersjp/development-kit/actions/runs/41063850666) (RED). Final diagnostic SHA `a21f91da7e62e88df7358e5c2153818800049938` keys all five conditional v0.12/v0.13 steps on the **canonical runtime file** and requires the v0.13 contract whenever that runtime exists; both full Ubuntu/Windows v0.13 lanes passed [CI #41064093123](https://github.com/eybersjp/development-kit/actions/runs/41064093123). Only the validated `.github/workflows/ci.yml` and `scripts/v071-regression.test.mjs` changes were ported to PR #92 in commits `fb25ff00fb3306f3ff5990c85c771291af12d328` and `a9d114f17ccb56be1795024f7b2cfa943d34b686`. The diagnostic PR remains draft and must not merge. **This is not T04 ACCEPTED:** PR #92 needs fresh exact-head CI, two distinct full validations on the eventual final reviewed SHA, authentic independent role reviews, closure of all unresolved P1 threads and Issue #73 physical NTFS sudden-power-loss evidence. Neither T04 nor v0.13 was merged or released. + - **T04-P1 prototype-safe patch replay (30 October 2026):** actual integration PR #92 independent review found inherited-property traversal in `diffValues`/`setAtPath`/`deleteAtPath`. Isolated diagnostic draft PR #109 test-only commit `692084d0b02194a39f563e1f90705077eb771f75` reproduced process-wide `Object.prototype` pollution in Ubuntu/Windows CI [#41057142898](https://github.com/eybersjp/development-kit/actions/runs/41057142898). Correction `7d55832a640400948d7cf896bcc18b822c223471` uses `Object.hasOwn` traversal and `Object.defineProperty` for literal JSON keys; both full v0.13 Ubuntu and Windows lanes passed [#41057264710](https://github.com/eybersjp/development-kit/actions/runs/41057264710). Only source/test changes were ported to PR #92 in commits `e92c2fd1df681751c2ff70e565ba5c5e1beaf731` and `42df7f1f58ba899e817618964a5975b5f8ace52f`; diagnostic CI exception was not ported. - **T04-P1 first-layout concurrent ledger loss (30 October 2026):** PR #92 review found `ensureStateEngineLayout` could rename an empty initial ledger over an already acknowledged concurrent writer. Isolated diagnostic draft PR #110 corrected its deterministic Linux descriptor-relative interleaving fixture at `079a92a9a8b8d9b379a7b774d5edcc2790c91d74`; test-only Ubuntu/Windows CI [#41057459611](https://github.com/eybersjp/development-kit/actions/runs/41057459611) reproduced loss of the acknowledged event (empty `events.jsonl`). Correction `d1b1d9e32e1ac74e4b8ee98fd7a1fdb7c6a79915` creates initial layout entries exclusively with pinned-directory identity and fsync barriers, never replacing an existing writer-created ledger. Both full Ubuntu and Windows v0.13 validation lanes passed [#41057594721](https://github.com/eybersjp/development-kit/actions/runs/41057594721). Only source/test changes were ported to PR #92 in commits `99fe01dd428f48bb7b4499213dbadde27b028f61` and `f9fde7c4ea56c1bd2be1a373c0ad7f9fcbb4337c`; diagnostic CI exception was not ported. - **Acceptance boundary:** the two diagnostic green runs validate separate source corrections, not final combined source acceptance. PR #92 must pass fresh exact-head full Ubuntu/Windows runs and independent distinct code/architecture/security review after this memory update; Issue #73 still requires controlled physical NTFS sudden-power-loss evidence. Do not merge, release or persist `ACCEPTED` without every risk-3 gate. From 7c199d0dc637663adee27a14428507c88ebbcd49 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 15:58:35 +0200 Subject: [PATCH 059/177] test(v013): port CI-reproduced entity identity and concurrent schema publication regressions --- scripts/state-engine-v2.test.mjs | 56 ++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/scripts/state-engine-v2.test.mjs b/scripts/state-engine-v2.test.mjs index 2bb98dedb..e1d9aa450 100644 --- a/scripts/state-engine-v2.test.mjs +++ b/scripts/state-engine-v2.test.mjs @@ -3,6 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { spawnSync } from 'node:child_process'; import { appendEntityState, @@ -10,6 +11,7 @@ import { cleanupOrphanedStateTemps, countStateEngineFiles, getStateEnginePaths, + ensureStateEngineLayout, loadCanonicalEvents, loadStateIndex, loadStateSnapshot, @@ -399,3 +401,57 @@ test('T04-P1 patch replay preserves own __proto__ data without prototype polluti delete Object.prototype[key]; } }); + + +test('V13-SEC-ENTITY-01 prototype-sensitive entity identities and index references are literal own keys', (t) => { + const rootDir = tempProject(t, 'dk-v13-identity-'); + const moduleUrl = new URL('../runtime/orchestration/state-engine-v2.mjs', import.meta.url).href; + const childCode = [ + 'import assert from "node:assert/strict";', + 'import { appendEntityState, loadStateSnapshot, loadEntityState, queryStateIndex, verifyStateEngineIntegrity } from ' + JSON.stringify(moduleUrl) + ';', + 'const rootDir = process.argv[1];', + 'for (const [type,id] of [["generic-state","__proto__"],["constructor","entity"],["generic-state","constructor"]]) {', + ' appendEntityState({ rootDir, entityType:type, entityId:id, state:{stateRevision:1,value:type+"/"+id}, refs:{contractId:"__proto__",taskId:"constructor"}, actorClass:"identity-security-test" });', + ' const state = loadEntityState(type,id,rootDir);', + ' assert.equal(state?.value, type+"/"+id);', + '}', + 'const snapshot = loadStateSnapshot(rootDir);', + 'assert.ok(Object.hasOwn(snapshot.entities, "constructor"));', + 'assert.ok(Object.hasOwn(snapshot.entities["generic-state"], "__proto__"));', + 'assert.ok(Object.hasOwn(snapshot.entities["generic-state"], "constructor"));', + 'assert.equal(queryStateIndex({rootDir,contractId:"__proto__"}).length, 3);', + 'assert.equal(queryStateIndex({rootDir,taskId:"constructor"}).length, 3);', + 'assert.equal(verifyStateEngineIntegrity(rootDir).valid, true);', + 'assert.equal(Object.hasOwn(Object.prototype, "state"), false);', + 'assert.equal(Object.hasOwn(Object.prototype, "stateRevision"), false);', + ].join("\n"); + const child = spawnSync(process.execPath, ['--input-type=module', '-e', childCode, rootDir], + { encoding: 'utf8', timeout: 20000 }); + assert.equal(child.status, 0, 'Identity replay must not pollute inherited objects or lose records: ' + child.stderr); +}); + +test('V13-CONC-SCHEMA-01 another initializer never observes an incomplete published schema', (t) => { + const rootDir = tempProject(t, 'dk-v13-schema-'); + const moduleUrl = new URL('../runtime/orchestration/state-engine-v2.mjs', import.meta.url).href; + const originalWrite = fs.writeFileSync; + let interleaved = false; + fs.writeFileSync = function interleaveSchemaWrite(fd, content, ...args) { + if (!interleaved && typeof fd === 'number' && typeof content === 'string' + && content.includes('canonicalHistory') && content.includes('materializedSnapshot')) { + interleaved = true; + const code = 'import {ensureStateEngineLayout} from ' + JSON.stringify(moduleUrl) + + '; ensureStateEngineLayout(process.argv[1]);'; + const child = spawnSync(process.execPath, ['--input-type=module', '-e', code, rootDir], + { encoding: 'utf8', timeout: 20000 }); + assert.equal(child.status, 0, 'Concurrent initializer must never read an empty schema: ' + child.stderr); + } + return originalWrite.call(this, fd, content, ...args); + }; + try { + ensureStateEngineLayout(rootDir); + } finally { + fs.writeFileSync = originalWrite; + } + assert.equal(interleaved, true, 'Fixture must interleave at schema publication'); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From e75c6225f1090225504c68815f56035e59f462a8 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 15:58:41 +0200 Subject: [PATCH 060/177] fix(v013): port tested own-key identity and atomic initial schema publication --- runtime/orchestration/state-engine-v2.mjs | 87 +++++++++++++++++------ 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 0744ae60d..dbb5f6aa4 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -506,6 +506,8 @@ function schemaDocument() { function createInitialStateFile(filePath, content) { const directory = path.dirname(filePath); const dirFd = fs.openSync(directory, 'r'); + let temporary = null; + let fd; try { const pinned = fs.fstatSync(dirFd); const observed = fs.lstatSync(directory); @@ -515,21 +517,49 @@ function createInitialStateFile(filePath, content) { } const root = path.dirname(path.dirname(directory)); assertStateRootRealpathSafe({ root, stateRoot: directory }); - const destination = process.platform === 'linux' - ? path.join('/proc/self/fd/' + dirFd, path.basename(filePath)) - : filePath; - let fd; - try { - fd = fs.openSync(destination, 'wx', 0o600); - } catch (error) { - if (error.code === 'EEXIST') return false; - throw error; - } - try { + const pinnedDirectory = process.platform === 'linux' ? '/proc/self/fd/' + dirFd : directory; + const destination = path.join(pinnedDirectory, path.basename(filePath)); + + if (content === '') { + // An empty ledger is already a complete valid file at exclusive create. + // O_EXCL ensures a competing writer's committed ledger is never replaced. + try { + fd = fs.openSync(destination, 'wx', 0o600); + } catch (error) { + if (error.code === 'EEXIST') return false; + throw error; + } + fs.fsyncSync(fd); + fs.closeSync(fd); + fd = undefined; + } else { + // Publishing a non-empty schema/snapshot/index with O_EXCL exposes an + // incomplete file to concurrent readers and leaves invalid state if the + // process dies before fsync. Prepare and fsync privately, then create + // the canonical name with an atomic, non-replacing same-directory link. + // No rename-overwrite is allowed; on unsupported filesystems fail closed. + temporary = path.join(pinnedDirectory, + path.basename(filePath) + '.tmp-' + process.pid + '-' + crypto.randomUUID()); + fd = fs.openSync(temporary, 'wx', 0o600); fs.writeFileSync(fd, content, 'utf8'); fs.fsyncSync(fd); - } finally { fs.closeSync(fd); + fd = undefined; + const beforePublish = fs.lstatSync(directory); + if (!beforePublish.isDirectory() || beforePublish.isSymbolicLink() || + beforePublish.dev !== pinned.dev || beforePublish.ino !== pinned.ino) { + throw new StateEngineError('State Engine directory changed before initial publication'); + } + try { + fs.linkSync(temporary, destination); + } catch (error) { + if (error.code === 'EEXIST') return false; + throw error; + } + // Remove the staging name immediately, before any observer can treat + // a temporary second hard link as an enduring canonical-file hazard. + fs.unlinkSync(temporary); + temporary = null; } const latest = fs.lstatSync(directory); if (!latest.isDirectory() || latest.isSymbolicLink() || @@ -539,6 +569,12 @@ function createInitialStateFile(filePath, content) { flushStateDirectory(directory, dirFd); return true; } finally { + if (fd !== undefined) fs.closeSync(fd); + if (temporary !== null) { + try { fs.unlinkSync(temporary); } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + } fs.closeSync(dirFd); } } @@ -877,8 +913,13 @@ function applyOperations(previousState, operations) { return next; } +function ownValue(object, key) { + return object !== null && object !== undefined && Object.hasOwn(object, key) + ? object[key] : undefined; +} + function ensureEntityBucket(snapshot, type) { - if (!snapshot.entities[type]) snapshot.entities[type] = {}; + if (!Object.hasOwn(snapshot.entities, type)) defineOwnPatchValue(snapshot.entities, type, {}); return snapshot.entities[type]; } @@ -893,10 +934,10 @@ export function rebuildSnapshotFromEvents(events) { if (event.eventType === 'ENTITY_STATE_INITIALIZED') { if (event.entityType === null || event.entityId === null) throw new StateEngineError('State initialization event requires entity identity'); const bucket = ensureEntityBucket(snapshot, event.entityType); - if (bucket[event.entityId]) throw new StateEngineError(`Entity is initialized more than once: ${event.entityType}/${event.entityId}`); + if (Object.hasOwn(bucket, event.entityId)) throw new StateEngineError(`Entity is initialized more than once: ${event.entityType}/${event.entityId}`); const state = cloneSerializable(event.payload.state, 'Initialized state'); const revision = stateRevision(event.payload.stateRevision); - bucket[event.entityId] = { + defineOwnPatchValue(bucket, event.entityId, { stateRevision: revision, state, lastEventSequence: event.sequence, @@ -907,11 +948,11 @@ export function rebuildSnapshotFromEvents(events) { contractId: event.contractId, runId: event.runId, }, - }; + }); } else if (event.eventType === 'ENTITY_STATE_UPDATED') { if (event.entityType === null || event.entityId === null) throw new StateEngineError('State update event requires entity identity'); const bucket = ensureEntityBucket(snapshot, event.entityType); - const current = bucket[event.entityId]; + const current = ownValue(bucket, event.entityId); if (!current) throw new StateEngineError(`State update references unknown entity: ${event.entityType}/${event.entityId}`); const revision = stateRevision(event.payload.stateRevision); if (revision < current.stateRevision) { @@ -977,7 +1018,7 @@ export function validateStateSnapshot(snapshot) { function indexArrayPush(map, key, value) { if (!key) return; - if (!map[key]) map[key] = []; + if (!Object.hasOwn(map, key)) defineOwnPatchValue(map, key, []); if (!map[key].includes(value)) map[key].push(value); } @@ -1528,7 +1569,7 @@ export function appendEntityState({ return withStateLock(rootDir, () => { const currentSnapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }) ?? emptySnapshot(); - const current = currentSnapshot.entities[normalizedType]?.[normalizedId] ?? null; + const current = ownValue(ownValue(currentSnapshot.entities, normalizedType), normalizedId) ?? null; if (current && revision < current.stateRevision) { throw new StateEngineError('Refusing stateRevision regression for ' + normalizedType + '/' + normalizedId); @@ -1587,14 +1628,14 @@ export function loadEntityState(type, id, rootDir = process.cwd()) { const normalizedType = entityType(type); const normalizedId = entityId(id); const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); - const record = snapshot?.entities?.[normalizedType]?.[normalizedId]; + const record = ownValue(ownValue(snapshot?.entities, normalizedType), normalizedId); return record ? structuredClone(record.state) : null; } export function listEntityRecords(type, rootDir = process.cwd()) { const normalizedType = entityType(type); const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); - const bucket = snapshot?.entities?.[normalizedType] ?? {}; + const bucket = ownValue(snapshot?.entities, normalizedType) ?? {}; return Object.entries(bucket) .map(([id, record]) => ({ entityId: id, @@ -1608,7 +1649,7 @@ export function listEntityRecords(type, rootDir = process.cwd()) { export function loadLatestEntityState(type, rootDir = process.cwd()) { const normalizedType = entityType(type); const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }); - const bucket = snapshot?.entities?.[normalizedType] ?? {}; + const bucket = ownValue(snapshot?.entities, normalizedType) ?? {}; const records = Object.entries(bucket) .map(([id, record]) => ({ id, record })) .sort((a, b) => b.record.lastEventSequence - a.record.lastEventSequence || a.id.localeCompare(b.id)); @@ -1647,7 +1688,7 @@ export function queryStateIndex({ let keys = null; for (const [field, value] of filters) { - const current = new Set(index[field][String(value)] ?? []); + const current = new Set(ownValue(index[field], String(value)) ?? []); keys = keys === null ? current : new Set([...keys].filter((key) => current.has(key))); } return index.records.filter((record) => keys.has(record.key)).map((record) => structuredClone(record)); From 19cf3bbee2cd2eeb49766e31e0aafd8a69ee9cfb Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 15:58:53 +0200 Subject: [PATCH 061/177] docs(memory): record test-first v0.13 entity identity and atomic schema CI evidence --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index c84298073..e54bb4cb1 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-P1 entity dictionary / P2 atomic schema publication (30 October 2026):** isolated diagnostic draft [PR #112](https://github.com/eybersjp/development-kit/pull/112) reproduced two distinct PR #92 review findings with test-only commit `8fbc216705e2f4bf736ef63cf160a163cc5181cd`. GitHub Actions [#41066475740](https://github.com/eybersjp/development-kit/actions/runs/41066475740) failed on **both Ubuntu and Windows**: inherited `__proto__`/`constructor` entity identities were misclassified as existing entities, and a second initializer observed a published but empty `schema.json`. The narrowly scoped correction uses own-property-safe entity buckets/lookups and index references, and prepares/fsyncs non-empty initial schema/snapshot/index privately before atomic, non-replacing hard-link publication; empty canonical ledger creation remains exclusive and non-replacing. Diagnostic SHA `16cd69e20a443eb75689bb3bc30fc9237bfb7f9a` passed the **complete Ubuntu and Windows v0.13 compatibility/regression jobs** in [CI #41066604563](https://github.com/eybersjp/development-kit/actions/runs/41066604563). Ported **only** the validated `scripts/state-engine-v2.test.mjs` and `runtime/orchestration/state-engine-v2.mjs` changes to actual PR #92 in commits `7c199d0dc637663adee27a14428507c88ebbcd49` and `e75c6225f1090225504c68815f56035e59f462a8`. PR #112 stays draft and must not merge. These tests do not prove Windows NTFS physical sudden-power-loss durability, independent source-bound reviews or deterministic risk-3 acceptance; Issue #73 and unresolved review threads remain blocking. New PR #92 source/memory commit requires fresh exact-head CI and authentic re-review. + - **T04-CI-01 source-bound v0.13 CI routing (30 October 2026):** actual integration PR #92 previously selected v0.13 full compatibility regression only when `github.head_ref` exactly equalled `feature/v0.13-state-engine-v2`. On a `main` push or renamed PR branch this condition could skip T04 validation despite the canonical runtime being present. Isolated diagnostic draft [PR #111](https://github.com/eybersjp/development-kit/pull/111) reproduced the branch-name bypass with a test-only regression in Ubuntu [CI #41062682013](https://github.com/eybersjp/development-kit/actions/runs/41062682013) (RED). A first source-bound correction passed Ubuntu/Windows [CI #41062788215](https://github.com/eybersjp/development-kit/actions/runs/41062788215) (GREEN), but the follow-on adversarial test proved that keying on the *contract file* alone allowed deletion of that contract to bypass T04 validation; [CI #41063850666](https://github.com/eybersjp/development-kit/actions/runs/41063850666) (RED). Final diagnostic SHA `a21f91da7e62e88df7358e5c2153818800049938` keys all five conditional v0.12/v0.13 steps on the **canonical runtime file** and requires the v0.13 contract whenever that runtime exists; both full Ubuntu/Windows v0.13 lanes passed [CI #41064093123](https://github.com/eybersjp/development-kit/actions/runs/41064093123). Only the validated `.github/workflows/ci.yml` and `scripts/v071-regression.test.mjs` changes were ported to PR #92 in commits `fb25ff00fb3306f3ff5990c85c771291af12d328` and `a9d114f17ccb56be1795024f7b2cfa943d34b686`. The diagnostic PR remains draft and must not merge. **This is not T04 ACCEPTED:** PR #92 needs fresh exact-head CI, two distinct full validations on the eventual final reviewed SHA, authentic independent role reviews, closure of all unresolved P1 threads and Issue #73 physical NTFS sudden-power-loss evidence. Neither T04 nor v0.13 was merged or released. - **T04-P1 prototype-safe patch replay (30 October 2026):** actual integration PR #92 independent review found inherited-property traversal in `diffValues`/`setAtPath`/`deleteAtPath`. Isolated diagnostic draft PR #109 test-only commit `692084d0b02194a39f563e1f90705077eb771f75` reproduced process-wide `Object.prototype` pollution in Ubuntu/Windows CI [#41057142898](https://github.com/eybersjp/development-kit/actions/runs/41057142898). Correction `7d55832a640400948d7cf896bcc18b822c223471` uses `Object.hasOwn` traversal and `Object.defineProperty` for literal JSON keys; both full v0.13 Ubuntu and Windows lanes passed [#41057264710](https://github.com/eybersjp/development-kit/actions/runs/41057264710). Only source/test changes were ported to PR #92 in commits `e92c2fd1df681751c2ff70e565ba5c5e1beaf731` and `42df7f1f58ba899e817618964a5975b5f8ace52f`; diagnostic CI exception was not ported. From 57744d530ee38d457dc945d01982c08cd4cacd83 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 16:56:59 +0200 Subject: [PATCH 062/177] fix(v0.13 T04): port CI-verified collision-free index identities and stale-index repair --- runtime/orchestration/state-engine-v2.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index dbb5f6aa4..77fbfea46 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -1032,7 +1032,9 @@ export function buildStateIndex(snapshot) { for (const [type, bucket] of Object.entries(snapshot.entities).sort(([a], [b]) => a.localeCompare(b))) { for (const [id, record] of Object.entries(bucket).sort(([a], [b]) => a.localeCompare(b))) { - const key = `${type}::${id}`; + // Encode the tuple rather than concatenating: both identity fields may contain '::'. + // This disposable index must never let a filter match another entity's record. + const key = JSON.stringify([type, id]); records.push({ key, entityType: type, @@ -1205,6 +1207,12 @@ export function loadStateIndex(rootDir = process.cwd(), { rebuildIfNeeded = true try { const index = JSON.parse(fs.readFileSync(paths.index, 'utf8')); validateStateIndex(index, snapshot); + // A matching sequence/hash only proves source freshness, not that the + // disposable reference maps were built with the current collision-free + // identity encoding. Reject stale or corrupted maps before any query. + if (stableJson(index) !== stableJson(buildStateIndex(snapshot))) { + throw new StateEngineError('State index is not semantically equivalent to canonical snapshot'); + } return index; } catch (error) { if (!rebuildIfNeeded) throw error; From 8dff8e451ab3cbebdefec7d9daeca8707112ccd2 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 16:57:06 +0200 Subject: [PATCH 063/177] test(v0.13 T04): port red/green CI-verified index collision and stale-index regressions --- scripts/state-engine-v2.test.mjs | 72 ++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/scripts/state-engine-v2.test.mjs b/scripts/state-engine-v2.test.mjs index e1d9aa450..bcf7ae7a0 100644 --- a/scripts/state-engine-v2.test.mjs +++ b/scripts/state-engine-v2.test.mjs @@ -455,3 +455,75 @@ test('V13-CONC-SCHEMA-01 another initializer never observes an incomplete publis assert.equal(interleaved, true, 'Fixture must interleave at schema publication'); assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); }); + +test('V13-INDEX-IDENTITY-01 composite entity keys cannot collide across colon-bearing identities', (t) => { + const rootDir = tempProject(t, 'dk-v13-index-identity-'); + const cases = [ + { entityType: 'a::b', entityId: 'c', contractId: 'contract-first', + taskId: 'task-first', runId: 'run-first', lifecycleId: 'lifecycle-first' }, + { entityType: 'a', entityId: 'b::c', contractId: 'contract-second', + taskId: 'task-second', runId: 'run-second', lifecycleId: 'lifecycle-second' }, + ]; + for (const entry of cases) { + appendEntityState({ + rootDir, + entityType: entry.entityType, + entityId: entry.entityId, + state: { stateRevision: 1, marker: entry.contractId }, + refs: { contractId: entry.contractId, taskId: entry.taskId, + runId: entry.runId, lifecycleId: entry.lifecycleId }, + actorClass: 'index-identity-regression', + }); + } + const index = loadStateIndex(rootDir); + assert.equal(index.records.length, 2); + assert.equal(new Set(index.records.map((record) => record.key)).size, 2, + 'Distinct valid entity tuples must never share the same index key'); + for (const entry of cases) { + for (const [filter, value] of [ + ['contractId', entry.contractId], ['taskId', entry.taskId], + ['runId', entry.runId], ['lifecycleId', entry.lifecycleId], + ]) { + const found = queryStateIndex({ rootDir, [filter]: value }); + assert.equal(found.length, 1, filter + ' must not return another entity with a colliding key'); + assert.equal(found[0].entityType, entry.entityType); + assert.equal(found[0].entityId, entry.entityId); + } + } + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + + +test('V13-INDEX-IDENTITY-02 stale delimiter-based index is rebuilt before serving filtered queries', (t) => { + const rootDir = tempProject(t, 'dk-v13-index-upgrade-'); + for (const entry of [ + { entityType: 'a::b', entityId: 'c', contractId: 'first' }, + { entityType: 'a', entityId: 'b::c', contractId: 'second' }, + ]) { + appendEntityState({ + rootDir, entityType: entry.entityType, entityId: entry.entityId, + state: { stateRevision: 1, marker: entry.contractId }, + refs: { contractId: entry.contractId }, + actorClass: 'index-upgrade-regression', + }); + } + const paths = getStateEnginePaths(rootDir); + const oldIndex = structuredClone(loadStateIndex(rootDir)); + oldIndex.records = oldIndex.records.map((record) => ({ + ...record, key: record.entityType + '::' + record.entityId, + })); + for (const map of [oldIndex.byContract, oldIndex.byTask, oldIndex.byRun, oldIndex.byLifecycle]) { + for (const [ref, keys] of Object.entries(map)) { + map[ref] = keys.map((key) => { + const record = loadStateIndex(rootDir).records.find((entry) => entry.key === key); + return record.entityType + '::' + record.entityId; + }); + } + } + fs.writeFileSync(paths.index, JSON.stringify(oldIndex) + '\n'); + const first = queryStateIndex({ rootDir, contractId: 'first' }); + assert.equal(first.length, 1, 'An old derived index must not leak another contract'); + assert.equal(first[0].entityType, 'a::b'); + assert.equal(first[0].entityId, 'c'); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From 9133ae84e7d5379e5f0e4f439d6ee84f235f25ed Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:03:14 +0200 Subject: [PATCH 064/177] docs(memory): preserve v0.13 T04 test-first index and bootstrap recovery evidence --- memory.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/memory.md b/memory.md index e54bb4cb1..1173f3542 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,9 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-P2 composite index identity collision (30 October 2026):** actual integration PR #92 review identified a collision between valid identities (`a::b`, `c`) and (`a`, `b::c`), leaking records across contract/task/run/lifecycle filters. Isolated draft diagnostic [PR #114](https://github.com/eybersjp/development-kit/pull/114) reproduced the defect with test-only SHA `85a97cfc55e17c3ef36c1364619330f04b73cf0c`: Ubuntu and Windows CI [#41074686949](https://github.com/eybersjp/development-kit/actions/runs/41074686949) RED. Follow-up stale-index test `cfe3656dd95a0cce68d73734171350757d60799d` reproduced a second failure on Ubuntu in [#41074990296](https://github.com/eybersjp/development-kit/actions/runs/41074990296): a legacy derived index with matching source hash was still trusted. Diagnostic SHA `dbba5619b783f8c2b5451f24766410847728b4e2` passed both complete Ubuntu/Windows v0.13 CI lanes [#41075160424](https://github.com/eybersjp/development-kit/actions/runs/41075160424). The narrow correction uses collision-free JSON tuple keys and compares loaded disposable indexes to the canonical rebuilt index, rebuilding stale mappings before serving queries. Ported only the verified runtime/test changes to actual PR #92 as `57744d530ee38d457dc945d01982c08cd4cacd83` and `8dff8e451ab3cbebdefec7d9daeca8707112ccd2`; production exact-head CI must be checked separately. PR #114 remains draft, never merge. This is not T04 ACCEPTED, does not resolve Windows physical power-loss evidence or independent risk-3 reviews. +- **T04-P2 interrupted bootstrap hard-link investigation (30 October 2026):** isolated draft [PR #115](https://github.com/eybersjp/development-kit/pull/115) reproduced an orphaned `schema.json.tmp-...` hard link after process death between atomic non-replacing link publication and staging unlink: Ubuntu CI [#41075297478](https://github.com/eybersjp/development-kit/actions/runs/41075297478) RED, normal reads fail on nlink=2. A narrowly confined candidate validates an exact initial bootstrap image, same inode/dev, precisely one private staging link and dead owner before unlink/flush; unrelated and live-owner links fail closed. Ubuntu diagnostic green at `c305318d6e00368bce37008500bc423741ac28b4`, but Windows [#41075671505](https://github.com/eybersjp/development-kit/actions/runs/41075671505) exposed H03/H15 writer-lock timeout under the additional normal-path filesystem probes. Optimized candidate `522c8b617f99051e655167f2f5754131b60709a7` probes for orphaned staging links **only when a multiple hard link is actually observed**; its complete Windows/Ubuntu CI must pass before any port to PR #92. Do not weaken concurrency or claim physical power-loss durability. + - **T04-P1 entity dictionary / P2 atomic schema publication (30 October 2026):** isolated diagnostic draft [PR #112](https://github.com/eybersjp/development-kit/pull/112) reproduced two distinct PR #92 review findings with test-only commit `8fbc216705e2f4bf736ef63cf160a163cc5181cd`. GitHub Actions [#41066475740](https://github.com/eybersjp/development-kit/actions/runs/41066475740) failed on **both Ubuntu and Windows**: inherited `__proto__`/`constructor` entity identities were misclassified as existing entities, and a second initializer observed a published but empty `schema.json`. The narrowly scoped correction uses own-property-safe entity buckets/lookups and index references, and prepares/fsyncs non-empty initial schema/snapshot/index privately before atomic, non-replacing hard-link publication; empty canonical ledger creation remains exclusive and non-replacing. Diagnostic SHA `16cd69e20a443eb75689bb3bc30fc9237bfb7f9a` passed the **complete Ubuntu and Windows v0.13 compatibility/regression jobs** in [CI #41066604563](https://github.com/eybersjp/development-kit/actions/runs/41066604563). Ported **only** the validated `scripts/state-engine-v2.test.mjs` and `runtime/orchestration/state-engine-v2.mjs` changes to actual PR #92 in commits `7c199d0dc637663adee27a14428507c88ebbcd49` and `e75c6225f1090225504c68815f56035e59f462a8`. PR #112 stays draft and must not merge. These tests do not prove Windows NTFS physical sudden-power-loss durability, independent source-bound reviews or deterministic risk-3 acceptance; Issue #73 and unresolved review threads remain blocking. New PR #92 source/memory commit requires fresh exact-head CI and authentic re-review. - **T04-CI-01 source-bound v0.13 CI routing (30 October 2026):** actual integration PR #92 previously selected v0.13 full compatibility regression only when `github.head_ref` exactly equalled `feature/v0.13-state-engine-v2`. On a `main` push or renamed PR branch this condition could skip T04 validation despite the canonical runtime being present. Isolated diagnostic draft [PR #111](https://github.com/eybersjp/development-kit/pull/111) reproduced the branch-name bypass with a test-only regression in Ubuntu [CI #41062682013](https://github.com/eybersjp/development-kit/actions/runs/41062682013) (RED). A first source-bound correction passed Ubuntu/Windows [CI #41062788215](https://github.com/eybersjp/development-kit/actions/runs/41062788215) (GREEN), but the follow-on adversarial test proved that keying on the *contract file* alone allowed deletion of that contract to bypass T04 validation; [CI #41063850666](https://github.com/eybersjp/development-kit/actions/runs/41063850666) (RED). Final diagnostic SHA `a21f91da7e62e88df7358e5c2153818800049938` keys all five conditional v0.12/v0.13 steps on the **canonical runtime file** and requires the v0.13 contract whenever that runtime exists; both full Ubuntu/Windows v0.13 lanes passed [CI #41064093123](https://github.com/eybersjp/development-kit/actions/runs/41064093123). Only the validated `.github/workflows/ci.yml` and `scripts/v071-regression.test.mjs` changes were ported to PR #92 in commits `fb25ff00fb3306f3ff5990c85c771291af12d328` and `a9d114f17ccb56be1795024f7b2cfa943d34b686`. The diagnostic PR remains draft and must not merge. **This is not T04 ACCEPTED:** PR #92 needs fresh exact-head CI, two distinct full validations on the eventual final reviewed SHA, authentic independent role reviews, closure of all unresolved P1 threads and Issue #73 physical NTFS sudden-power-loss evidence. Neither T04 nor v0.13 was merged or released. From 879d77379de868064bfab6dae8efc5b7b873f053 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:09:09 +0200 Subject: [PATCH 065/177] fix(v0.13 T04): port CI-verified orphaned bootstrap hardlink recovery with fast-path concurrency preserved --- runtime/orchestration/state-engine-v2.mjs | 91 ++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 77fbfea46..923f20d6c 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -145,7 +145,15 @@ function assertStateFilesNotLinks(paths) { throw new StateEngineError('State Engine internal file is not a regular file: ' + field); } if (stat.nlink > 1) { - throw new StateEngineError('State Engine internal file has multiple hard links: ' + field); + // The common path must not pay for extra filesystem probes on every + // state read/write. Reconcile only an actual multiply-linked bootstrap + // schema/snapshot/index; all other hard links remain forbidden. + if (['schema', 'snapshot', 'index'].includes(field)) { + recoverInterruptedBootstrapPublication(paths); + } + if (fs.lstatSync(paths[field]).nlink > 1) { + throw new StateEngineError('State Engine internal file has multiple hard links: ' + field); + } } } catch (error) { if (error.code !== 'ENOENT') throw error; @@ -153,6 +161,87 @@ function assertStateFilesNotLinks(paths) { } } +/** + * Recover only the exact private staging hard link left by a terminated + * first-layout initializer between linkSync() and unlinkSync(). No other + * multiply-linked canonical file is eligible. A live/unknown owner, unknown + * content, non-regular path, or different inode remains a hard failure. + * This does not relax the trusted/exclusive local-runtime threat boundary. + */ +function recoverInterruptedBootstrapPublication(paths) { + for (const field of ['schema', 'snapshot', 'index']) { + const canonical = paths[field]; + let stat; + try { stat = fs.lstatSync(canonical, { bigint: true }); } + catch (error) { + if (error.code === 'ENOENT') continue; + throw error; + } + if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 2n) continue; + + const prefix = path.basename(canonical) + '.tmp-'; + const matches = []; + for (const name of fs.readdirSync(paths.stateRoot)) { + if (!name.startsWith(prefix)) continue; + const suffix = name.slice(prefix.length); + const ownerMatch = /^([1-9][0-9]*)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/.exec(suffix); + if (!ownerMatch) continue; + const candidate = path.join(paths.stateRoot, name); + const candidateStat = fs.lstatSync(candidate, { bigint: true }); + if (candidateStat.isFile() && !candidateStat.isSymbolicLink() + && candidateStat.dev === stat.dev && candidateStat.ino === stat.ino + && candidateStat.nlink === 2n) { + matches.push({ name, candidate, pid: Number(ownerMatch[1]) }); + } + } + if (matches.length !== 1 || !Number.isSafeInteger(matches[0].pid)) { + throw new StateEngineError('Ambiguous initial State Engine hard links; operator recovery required'); + } + const { name, candidate, pid } = matches[0]; + // A still-running initializer may be about to remove its own staging + // link. Never unlink it on that process's behalf. + let ownerDead = false; + try { process.kill(pid, 0); } + catch (error) { + if (error.code === 'ESRCH') ownerDead = true; + else throw new StateEngineError('Cannot establish bootstrap staging owner termination'); + } + if (!ownerDead) { + throw new StateEngineError('Initial State Engine publication is still owned by a live process'); + } + + const expected = field === 'schema' ? stablePretty(schemaDocument()) + : field === 'snapshot' ? stablePretty(emptySnapshot()) + : stablePretty(buildStateIndex(emptySnapshot())); + if (fs.readFileSync(canonical, 'utf8') !== expected + || fs.readFileSync(candidate, 'utf8') !== expected) { + throw new StateEngineError('Initial State Engine staging content is not an exact bootstrap image'); + } + + const dirFd = fs.openSync(paths.stateRoot, 'r'); + try { + const pinned = fs.fstatSync(dirFd, { bigint: true }); + const observed = fs.lstatSync(paths.stateRoot, { bigint: true }); + const current = fs.lstatSync(canonical, { bigint: true }); + const staging = fs.lstatSync(candidate, { bigint: true }); + if (!pinned.isDirectory() || !observed.isDirectory() || observed.isSymbolicLink() + || pinned.dev !== observed.dev || pinned.ino !== observed.ino + || !current.isFile() || current.isSymbolicLink() + || current.dev !== stat.dev || current.ino !== stat.ino || current.nlink !== 2n + || !staging.isFile() || staging.isSymbolicLink() + || staging.dev !== stat.dev || staging.ino !== stat.ino || staging.nlink !== 2n) { + throw new StateEngineError('Initial State Engine staging identity changed before recovery'); + } + const unlinkTarget = process.platform === 'linux' + ? path.join('/proc/self/fd/' + dirFd, name) : candidate; + fs.unlinkSync(unlinkTarget); + flushStateDirectory(paths.stateRoot, dirFd); + } finally { + fs.closeSync(dirFd); + } + } +} + /** * Persist directory namespace updates before journal acknowledgement. * POSIX uses the pinned directory FD; Windows must open a write-capable From 410298fda1c8027a214bb2a235f56442ea986a27 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:09:17 +0200 Subject: [PATCH 066/177] test(v0.13 T04): port Windows/Ubuntu-verified orphan, external-link and live-owner recovery tests --- scripts/state-engine-v2.test.mjs | 52 ++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/scripts/state-engine-v2.test.mjs b/scripts/state-engine-v2.test.mjs index bcf7ae7a0..bafd44c8a 100644 --- a/scripts/state-engine-v2.test.mjs +++ b/scripts/state-engine-v2.test.mjs @@ -527,3 +527,55 @@ test('V13-INDEX-IDENTITY-02 stale delimiter-based index is rebuilt before servin assert.equal(first[0].entityId, 'c'); assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); }); + +test('V13-CONC-BOOTSTRAP-02 interrupted initial hard-link publication is safely recoverable', (t) => { + const rootDir = tempProject(t, 'dk-v13-bootstrap-link-'); + const moduleUrl = new URL('../runtime/orchestration/state-engine-v2.mjs', import.meta.url).href; + const childCode = [ + 'import fs from "node:fs";', + 'import { ensureStateEngineLayout } from ' + JSON.stringify(moduleUrl) + ';', + 'const link = fs.linkSync;', + 'fs.linkSync = (...args) => { link(...args); process.exit(77); };', + 'ensureStateEngineLayout(process.argv[1]);', + 'process.exit(99);', + ].join('\n'); + const child = spawnSync(process.execPath, ['--input-type=module', '-e', childCode, rootDir], + { encoding: 'utf8', timeout: 20000 }); + assert.equal(child.status, 77, 'Fixture must terminate after publication but before staging unlink: ' + child.stderr); + const stateRoot = path.join(rootDir, '.development-kit', 'state'); + const tempNames = fs.readdirSync(stateRoot).filter((name) => name.startsWith('schema.json.tmp-')); + assert.equal(tempNames.length, 1, 'Interrupted bootstrap staging link must remain'); + assert.equal(fs.statSync(path.join(stateRoot, 'schema.json')).nlink, 2); + ensureStateEngineLayout(rootDir); + assert.equal(fs.statSync(path.join(stateRoot, 'schema.json')).nlink, 1); + assert.equal(fs.existsSync(path.join(stateRoot, tempNames[0])), false); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + +test('V13-CONC-BOOTSTRAP-03 unrelated hard links remain fail-closed and preserved', (t) => { + const rootDir = tempProject(t, 'dk-v13-bootstrap-untrusted-'); + const paths = ensureStateEngineLayout(rootDir); + const external = path.join(rootDir, 'unrelated-schema-link'); + fs.linkSync(paths.schema, external); + assert.throws(() => ensureStateEngineLayout(rootDir), /hard links|multiple hard links/i); + assert.equal(fs.existsSync(external), true, 'Unrelated hard link must never be deleted'); + assert.equal(fs.statSync(paths.schema).nlink, 2); +}); + + +test('V13-CONC-BOOTSTRAP-04 live initializer staging links are not reclaimed', (t) => { + const rootDir = tempProject(t, 'dk-v13-bootstrap-live-'); + const paths = ensureStateEngineLayout(rootDir); + const temporary = path.join(paths.stateRoot, + 'schema.json.tmp-' + process.pid + '-00000000-0000-4000-8000-000000000001'); + fs.linkSync(paths.schema, temporary); + try { + assert.throws(() => ensureStateEngineLayout(rootDir), /live process/i, + 'A live initializer retains ownership of its temporary publication link'); + assert.equal(fs.existsSync(temporary), true); + assert.equal(fs.statSync(paths.schema).nlink, 2); + } finally { + fs.unlinkSync(temporary); + } + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From 73603f8a2004100b0fb8e302f89825eecc5638b5 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:09:26 +0200 Subject: [PATCH 067/177] docs(memory): record verified Windows/Ubuntu bootstrap recovery and integration commits --- memory.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/memory.md b/memory.md index 1173f3542..98c019454 100644 --- a/memory.md +++ b/memory.md @@ -11,8 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) -- **T04-P2 composite index identity collision (30 October 2026):** actual integration PR #92 review identified a collision between valid identities (`a::b`, `c`) and (`a`, `b::c`), leaking records across contract/task/run/lifecycle filters. Isolated draft diagnostic [PR #114](https://github.com/eybersjp/development-kit/pull/114) reproduced the defect with test-only SHA `85a97cfc55e17c3ef36c1364619330f04b73cf0c`: Ubuntu and Windows CI [#41074686949](https://github.com/eybersjp/development-kit/actions/runs/41074686949) RED. Follow-up stale-index test `cfe3656dd95a0cce68d73734171350757d60799d` reproduced a second failure on Ubuntu in [#41074990296](https://github.com/eybersjp/development-kit/actions/runs/41074990296): a legacy derived index with matching source hash was still trusted. Diagnostic SHA `dbba5619b783f8c2b5451f24766410847728b4e2` passed both complete Ubuntu/Windows v0.13 CI lanes [#41075160424](https://github.com/eybersjp/development-kit/actions/runs/41075160424). The narrow correction uses collision-free JSON tuple keys and compares loaded disposable indexes to the canonical rebuilt index, rebuilding stale mappings before serving queries. Ported only the verified runtime/test changes to actual PR #92 as `57744d530ee38d457dc945d01982c08cd4cacd83` and `8dff8e451ab3cbebdefec7d9daeca8707112ccd2`; production exact-head CI must be checked separately. PR #114 remains draft, never merge. This is not T04 ACCEPTED, does not resolve Windows physical power-loss evidence or independent risk-3 reviews. -- **T04-P2 interrupted bootstrap hard-link investigation (30 October 2026):** isolated draft [PR #115](https://github.com/eybersjp/development-kit/pull/115) reproduced an orphaned `schema.json.tmp-...` hard link after process death between atomic non-replacing link publication and staging unlink: Ubuntu CI [#41075297478](https://github.com/eybersjp/development-kit/actions/runs/41075297478) RED, normal reads fail on nlink=2. A narrowly confined candidate validates an exact initial bootstrap image, same inode/dev, precisely one private staging link and dead owner before unlink/flush; unrelated and live-owner links fail closed. Ubuntu diagnostic green at `c305318d6e00368bce37008500bc423741ac28b4`, but Windows [#41075671505](https://github.com/eybersjp/development-kit/actions/runs/41075671505) exposed H03/H15 writer-lock timeout under the additional normal-path filesystem probes. Optimized candidate `522c8b617f99051e655167f2f5754131b60709a7` probes for orphaned staging links **only when a multiple hard link is actually observed**; its complete Windows/Ubuntu CI must pass before any port to PR #92. Do not weaken concurrency or claim physical power-loss durability. +- **T04-P2 composite index identity collision (30 October 2026):** actual integration PR #92 review identified a collision between valid identities (`a::b`, `c`) and (`a`, `b::c`), leaking records across contract/task/run/lifecycle filters. Isolated draft diagnostic [PR #114](https://github.com/eybersjp/development-kit/pull/114) reproduced the defect with test-only SHA `85a97cfc55e17c3ef36c1364619330f04b73cf0c`: Ubuntu and Windows CI [#41074686949](https://github.com/eybersjp/development-kit/actions/runs/41074686949) RED. Follow-up stale-index test `cfe3656dd95a0cce68d73734171350757d60799d` reproduced a second failure on Ubuntu in [#41074990296](https://github.com/eybersjp/development-kit/actions/runs/41074990296): a legacy derived index with matching source hash was still trusted. Diagnostic SHA `dbba5619b783f8c2b5451f24766410847728b4e2` passed both complete Ubuntu/Windows v0.13 CI lanes [#41075160424](https://github.com/eybersjp/development-kit/actions/runs/41075160424). The narrow correction uses collision-free JSON tuple keys and compares loaded disposable indexes to the canonical rebuilt index, rebuilding stale mappings before serving queries. Ported only the verified runtime/test changes to actual PR #92 as `57744d530ee38d457dc945d01982c08cd4cacd83` and `8dff8e451ab3cbebdefec7d9daeca8707112ccd2`; production source/test SHA `8dff8e451ab3cbebdefec7d9daeca8707112ccd2` passed both full Ubuntu/Windows v0.13 jobs in [CI #41076212580](https://github.com/eybersjp/development-kit/actions/runs/41076212580); the later combined head requires its own fresh CI. PR #114 remains draft, never merge. This is not T04 ACCEPTED, does not resolve Windows physical power-loss evidence or independent risk-3 reviews. +- **T04-P2 interrupted bootstrap hard-link investigation (30 October 2026):** isolated draft [PR #115](https://github.com/eybersjp/development-kit/pull/115) reproduced an orphaned `schema.json.tmp-...` hard link after process death between atomic non-replacing link publication and staging unlink: Ubuntu CI [#41075297478](https://github.com/eybersjp/development-kit/actions/runs/41075297478) RED, normal reads fail on nlink=2. A narrowly confined candidate validates an exact initial bootstrap image, same inode/dev, precisely one private staging link and dead owner before unlink/flush; unrelated and live-owner links fail closed. Ubuntu diagnostic green at `c305318d6e00368bce37008500bc423741ac28b4`, but Windows [#41075671505](https://github.com/eybersjp/development-kit/actions/runs/41075671505) exposed H03/H15 writer-lock timeout under the additional normal-path filesystem probes. Optimized candidate `522c8b617f99051e655167f2f5754131b60709a7` probes for orphaned staging links **only when a multiple hard link is actually observed**; passed both full Ubuntu/Windows v0.13 jobs in [CI #41076958389](https://github.com/eybersjp/development-kit/actions/runs/41076958389), including H03/H15 Windows writer-lock concurrency. Ported only the validated bootstrap source/test changes to actual PR #92 in `879d77379de868064bfab6dae8efc5b7b873f053` and `410298fda1c8027a214bb2a235f56442ea986a27`; the diagnostic PR #115 remains draft and unmerged. The combined final PR #92 source requires fresh exact-head CI, external reviews and real NTFS sudden-power-loss assurance. Do not weaken concurrency or claim physical power-loss durability. - **T04-P1 entity dictionary / P2 atomic schema publication (30 October 2026):** isolated diagnostic draft [PR #112](https://github.com/eybersjp/development-kit/pull/112) reproduced two distinct PR #92 review findings with test-only commit `8fbc216705e2f4bf736ef63cf160a163cc5181cd`. GitHub Actions [#41066475740](https://github.com/eybersjp/development-kit/actions/runs/41066475740) failed on **both Ubuntu and Windows**: inherited `__proto__`/`constructor` entity identities were misclassified as existing entities, and a second initializer observed a published but empty `schema.json`. The narrowly scoped correction uses own-property-safe entity buckets/lookups and index references, and prepares/fsyncs non-empty initial schema/snapshot/index privately before atomic, non-replacing hard-link publication; empty canonical ledger creation remains exclusive and non-replacing. Diagnostic SHA `16cd69e20a443eb75689bb3bc30fc9237bfb7f9a` passed the **complete Ubuntu and Windows v0.13 compatibility/regression jobs** in [CI #41066604563](https://github.com/eybersjp/development-kit/actions/runs/41066604563). Ported **only** the validated `scripts/state-engine-v2.test.mjs` and `runtime/orchestration/state-engine-v2.mjs` changes to actual PR #92 in commits `7c199d0dc637663adee27a14428507c88ebbcd49` and `e75c6225f1090225504c68815f56035e59f462a8`. PR #112 stays draft and must not merge. These tests do not prove Windows NTFS physical sudden-power-loss durability, independent source-bound reviews or deterministic risk-3 acceptance; Issue #73 and unresolved review threads remain blocking. New PR #92 source/memory commit requires fresh exact-head CI and authentic re-review. From 385f7c9edc3fd2e882edcc31589f6c20de467124 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:24:19 +0200 Subject: [PATCH 068/177] fix(v0.13 T04): port CI-verified pending event validation before canonical recovery mutation --- runtime/orchestration/state-engine-v2.mjs | 37 ++++++++++++----------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 923f20d6c..2580f314e 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -1488,6 +1488,25 @@ function recoverPendingCommitLocked(rootDir) { if (!expectedSuffix.subarray(0, existingSuffix.length).equals(existingSuffix)) { throw new StateEngineError('Canonical state suffix conflicts with pending commit; refusing recovery'); } + canonical = rebuildSnapshotFromEvents( + parseLedger(Buffer.concat([bytes.subarray(0, pending.previousByteLength), expectedSuffix]).toString('utf8')), + ); + // Validate the entire proposed ledger and the materialized snapshot witness + // before any truncate/append/fsync can mutate canonical crash evidence. + let witness = null; + try { + witness = JSON.parse(fs.readFileSync(snapshotTarget, 'utf8')); + validateStateSnapshot(witness); + } catch { + witness = null; + } + if (witness && witness.lastEventSequence > canonical.lastEventSequence) { + throw new StateEngineError('Cannot recover a pending commit over a later snapshot integrity witness'); + } + if (witness && witness.lastEventSequence === canonical.lastEventSequence + && witness.lastEventHash !== canonical.lastEventHash) { + throw new StateEngineError('Pending recovery conflicts with snapshot integrity witness'); + } if (existingSuffix.length < expectedSuffix.length) { // Truncate and repair through this FD, never a separately resolved path. fs.ftruncateSync(fd, pending.previousByteLength); @@ -1511,24 +1530,6 @@ function recoverPendingCommitLocked(rootDir) { finalEntry.dev !== finalStat.dev || finalEntry.ino !== finalStat.ino) { throw new StateEngineError('Canonical ledger directory entry was displaced during pending recovery'); } - canonical = rebuildSnapshotFromEvents( - parseLedger(Buffer.concat([bytes.subarray(0, pending.previousByteLength), expectedSuffix]).toString('utf8')), - ); - // Reconstruct both derived views before acknowledging the durable commit. - let witness = null; - try { - witness = JSON.parse(fs.readFileSync(snapshotTarget, 'utf8')); - validateStateSnapshot(witness); - } catch { - witness = null; - } - if (witness && witness.lastEventSequence > canonical.lastEventSequence) { - throw new StateEngineError('Cannot recover a pending commit over a later snapshot integrity witness'); - } - if (witness && witness.lastEventSequence === canonical.lastEventSequence - && witness.lastEventHash !== canonical.lastEventHash) { - throw new StateEngineError('Pending recovery conflicts with snapshot integrity witness'); - } persistDerived(canonical, rootDir, dirFd); pinnedUnlinkAndSync(paths.pending, dirFd); } finally { From 130bb5c7ff05a2762bbf0a20b3ba2f444742e04a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:24:27 +0200 Subject: [PATCH 069/177] test(v0.13 T04): port CI-verified H52 evidence-preserving pending recovery regression --- scripts/state-engine-v2-hardening.test.mjs | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index 8e3b41571..9c68619e5 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -1082,3 +1082,31 @@ test('T04-P1 initial layout cannot overwrite an acknowledged concurrent first ev assert.equal(loadCanonicalEvents(rootDir).length, 1); assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); }); + +test('T04-H52 corrupted pending suffix is rejected before mutating canonical bytes or journal', (t) => { + for (const partialLength of [0, 20]) { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H52_BASELINE', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const baseline = fs.readFileSync(paths.events, 'utf8'); + appendMetadataEvent({ rootDir, eventType: 'H52_FOLLOWUP', payload: { value: 2 } }); + const validSuffix = fs.readFileSync(paths.events, 'utf8').slice(baseline.length); + const invalidEvent = JSON.parse(validSuffix.trim()); + invalidEvent.eventHash = 'sha256:' + '0'.repeat(64); + const invalidSuffix = JSON.stringify(invalidEvent) + '\n'; + const originalLedger = baseline + invalidSuffix.slice(0, partialLength); + const journal = JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(baseline), + previousHash: 'sha256:' + createHash('sha256').update(baseline).digest('hex'), + suffix: invalidSuffix, + }) + '\n'; + fs.writeFileSync(paths.events, originalLedger, 'utf8'); + fs.writeFileSync(paths.pending, journal, 'utf8'); + assert.throws(() => loadCanonicalEvents(rootDir), /event hash mismatch|invalid|integrity/i); + assert.equal(fs.readFileSync(paths.events, 'utf8'), originalLedger, + 'Invalid pending event must not overwrite original crash evidence (partialLength=' + partialLength + ')'); + assert.equal(fs.readFileSync(paths.pending, 'utf8'), journal, + 'Rejected pending journal must remain available for operator diagnosis'); + } +}); From 5150fb3011d7ffbd345725db1fab4cbef4158caa Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:24:38 +0200 Subject: [PATCH 070/177] docs(memory): record test-first pending suffix evidence preservation and remaining API projection P2 --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index 98c019454..9e755f2a4 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-P2 pending suffix validation before recovery mutation (30 October 2026):** independent code review on PR #92 SHA `73603f8a2004100b0fb8e302f89825eecc5638b5` found that `recoverPendingCommitLocked()` could truncate/append/fsync canonical bytes before checking a hash-invalid pending event or conflicting snapshot witness. Isolated draft diagnostic [PR #117](https://github.com/eybersjp/development-kit/pull/117) test-only `5d02b5d03b46f9e24a637f4a1696fc9e407ab20c` reproduced evidence destruction with both zero-byte and partially appended suffix fixtures in Ubuntu [CI #41079260840](https://github.com/eybersjp/development-kit/actions/runs/41079260840) (RED). Narrow source correction moves full proposed-ledger hash-chain validation and snapshot-witness checks before any canonical mutation; diagnostic SHA `e884d88fb3a11c59acc77f159dea209de4cd730e` passed both complete Ubuntu and Windows v0.13 lanes in [CI #41079450641](https://github.com/eybersjp/development-kit/actions/runs/41079450641). Ported only validated runtime/test to actual PR #92 in `385f7c9edc3fd2e882edcc31589f6c20de467124` and `130bb5c7ff05a2762bbf0a20b3ba2f444742e04a`; new combined-head CI and exact-source re-review remain mandatory. PR #117 remains draft, unmerged. The other new P2 review finding is stale `/v1/scale-context` API projection after V2 cutover, which needs a separate compatibility correction without reintroducing legacy `current.json` for fresh V2 projects. No physical NTFS power-loss result or independent risk-3 ACCEPTED verdict is claimed. + - **T04-P2 composite index identity collision (30 October 2026):** actual integration PR #92 review identified a collision between valid identities (`a::b`, `c`) and (`a`, `b::c`), leaking records across contract/task/run/lifecycle filters. Isolated draft diagnostic [PR #114](https://github.com/eybersjp/development-kit/pull/114) reproduced the defect with test-only SHA `85a97cfc55e17c3ef36c1364619330f04b73cf0c`: Ubuntu and Windows CI [#41074686949](https://github.com/eybersjp/development-kit/actions/runs/41074686949) RED. Follow-up stale-index test `cfe3656dd95a0cce68d73734171350757d60799d` reproduced a second failure on Ubuntu in [#41074990296](https://github.com/eybersjp/development-kit/actions/runs/41074990296): a legacy derived index with matching source hash was still trusted. Diagnostic SHA `dbba5619b783f8c2b5451f24766410847728b4e2` passed both complete Ubuntu/Windows v0.13 CI lanes [#41075160424](https://github.com/eybersjp/development-kit/actions/runs/41075160424). The narrow correction uses collision-free JSON tuple keys and compares loaded disposable indexes to the canonical rebuilt index, rebuilding stale mappings before serving queries. Ported only the verified runtime/test changes to actual PR #92 as `57744d530ee38d457dc945d01982c08cd4cacd83` and `8dff8e451ab3cbebdefec7d9daeca8707112ccd2`; production source/test SHA `8dff8e451ab3cbebdefec7d9daeca8707112ccd2` passed both full Ubuntu/Windows v0.13 jobs in [CI #41076212580](https://github.com/eybersjp/development-kit/actions/runs/41076212580); the later combined head requires its own fresh CI. PR #114 remains draft, never merge. This is not T04 ACCEPTED, does not resolve Windows physical power-loss evidence or independent risk-3 reviews. - **T04-P2 interrupted bootstrap hard-link investigation (30 October 2026):** isolated draft [PR #115](https://github.com/eybersjp/development-kit/pull/115) reproduced an orphaned `schema.json.tmp-...` hard link after process death between atomic non-replacing link publication and staging unlink: Ubuntu CI [#41075297478](https://github.com/eybersjp/development-kit/actions/runs/41075297478) RED, normal reads fail on nlink=2. A narrowly confined candidate validates an exact initial bootstrap image, same inode/dev, precisely one private staging link and dead owner before unlink/flush; unrelated and live-owner links fail closed. Ubuntu diagnostic green at `c305318d6e00368bce37008500bc423741ac28b4`, but Windows [#41075671505](https://github.com/eybersjp/development-kit/actions/runs/41075671505) exposed H03/H15 writer-lock timeout under the additional normal-path filesystem probes. Optimized candidate `522c8b617f99051e655167f2f5754131b60709a7` probes for orphaned staging links **only when a multiple hard link is actually observed**; passed both full Ubuntu/Windows v0.13 jobs in [CI #41076958389](https://github.com/eybersjp/development-kit/actions/runs/41076958389), including H03/H15 Windows writer-lock concurrency. Ported only the validated bootstrap source/test changes to actual PR #92 in `879d77379de868064bfab6dae8efc5b7b873f053` and `410298fda1c8027a214bb2a235f56442ea986a27`; the diagnostic PR #115 remains draft and unmerged. The combined final PR #92 source requires fresh exact-head CI, external reviews and real NTFS sudden-power-loss assurance. Do not weaken concurrency or claim physical power-loss durability. From 5920cd352dae245d6360a59fd62672c1259bccb9 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:26:57 +0200 Subject: [PATCH 071/177] test(v0.13 T04): port CI-verified W73-28 Unicode alias restore regression --- scripts/state-engine-v2-migration.test.mjs | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index 4baec1569..aaff7a6ed 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -1138,3 +1138,54 @@ test('W73-27 Windows 8.3 ancestor alias cannot bypass an interrupted restore jou assert.deepEqual(fs.readFileSync(path.join(targetRoot, 'original.txt'), 'utf8'), 'KEEP-ORIGINAL'); assert.deepEqual(fs.readFileSync(path.join(parent, journals[0])), journalBytes); }); + + +test('W73-28 Unicode uppercase-folded restore aliases fail closed before publication on every OS', async (t) => { + // NTFS case-insensitive name comparison uses an uppercase mapping. A + // conservative preflight must also reject these aliases on POSIX so the + // signed backup cannot behave differently on a Windows restore target. + const longS = '\u017f'; // LATIN SMALL LETTER LONG S; uppercase maps to S. + assert.equal(longS.toUpperCase(), 'S'); + assert.notEqual(longS.toLowerCase(), 's'); + + const source = tempProject(t, 'dk-ntfs-unicode-alias-source-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const backup = JSON.parse(fs.readFileSync(migrated.backupPath, 'utf8')); + const first = backup.files[0]; + const firstBytes = Buffer.from('FIRST-RESTORED-ENTRY'); + const secondBytes = Buffer.from('SECOND-RESTORED-ENTRY'); + const { createHash } = await import('node:crypto'); + const digest = bytes => 'sha256:' + createHash('sha256').update(bytes).digest('hex'); + const relativeDir = '.development-kit/autopilot/state/'; + backup.files.push({ + ...first, path: relativeDir + 's.json', + bytes: firstBytes.length, fingerprint: digest(firstBytes), + contentBase64: firstBytes.toString('base64'), + }); + backup.files.push({ + ...first, path: relativeDir + longS + '.json', + bytes: secondBytes.length, fingerprint: digest(secondBytes), + contentBase64: secondBytes.toString('base64'), + }); + backup.fileCount = backup.files.length; + const stable = value => Array.isArray(value) ? value.map(stable) + : value && typeof value === 'object' + ? Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])) + : value; + const manifest = backup.files.map(e => ({ path: e.path, fingerprint: e.fingerprint, bytes: e.bytes })); + backup.sourceFingerprint = digest(JSON.stringify(stable(manifest))); + const hostileBackup = path.join(source, 'ntfs-alias-backup.json'); + fs.writeFileSync(hostileBackup, JSON.stringify(backup)); + const targetRoot = tempProject(t, 'dk-ntfs-unicode-alias-target-'); + const original = path.join(targetRoot, 'original.txt'); + fs.writeFileSync(original, 'KEEP-ORIGINAL'); + assert.throws( + () => restoreLegacyBackup({ backupPath: hostileBackup, targetRoot, confirmOffline: true }), + /duplicate|alias|collision|physical identity/i, + 'Two different backup paths naming one physical NTFS file must fail closed before publication', + ); + assert.equal(fs.readFileSync(original, 'utf8'), 'KEEP-ORIGINAL'); + assert.equal(fs.existsSync(path.join(targetRoot, relativeDir, 's.json')), false, + 'Rejected backup must not publish the staged candidate'); +}); From ea32b2bbff336f6dde4f581c2315b58f4a7e86b0 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:27:04 +0200 Subject: [PATCH 072/177] fix(v0.13 T04): port Windows/Ubuntu CI-verified restore path alias safeguards --- .../orchestration/state-engine-migration.mjs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index fea7b1b5d..ac3e030e1 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -1606,7 +1606,12 @@ export function restoreLegacyBackup({ for (const { entry, destination } of destinations) { const key = destination .normalize('NFC') - .toLowerCase() + // NTFS filename collation is based on uppercase mappings; lowercase + // folding misses valid aliases such as U+017F (long s) versus s. + // This is conservative only: actual NTFS $UpCase tables may differ + // from JavaScript Unicode, so the staged physical identity guard below + // remains mandatory for 8.3 and volume-specific aliases. + .toUpperCase() .split(/[\\/]+/) .map((part) => part.replace(/[. ]+$/, '')) .join('/'); @@ -1665,8 +1670,20 @@ export function restoreLegacyBackup({ } const restored = []; + // A second signed path can name an already restored physical file via + // NTFS 8.3 or volume-specific $UpCase aliases even when string folding + // finds no duplicate. Check the file identity before every replacement. + const writtenStageFileIds = new Set(); for (const { entry } of destinations) { const destination = resolveSafeRestoreDestination(stage, entry.path, { createMissing: true }); + try { + const existing = fs.lstatSync(destination, { bigint: true }); + if (writtenStageFileIds.has(existing.dev.toString() + ':' + existing.ino.toString())) { + throw new StateMigrationError('Duplicate physical restore destination alias: ' + entry.path); + } + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } const content = Buffer.from(entry.contentBase64, 'base64'); const tempFile = destination + '.tmp-' + crypto.randomUUID(); const fd = fs.openSync(tempFile, 'wx', 0o600); @@ -1677,6 +1694,8 @@ export function restoreLegacyBackup({ fs.closeSync(fd); } fs.renameSync(tempFile, destination); + const writtenIdentity = fs.lstatSync(destination, { bigint: true }); + writtenStageFileIds.add(writtenIdentity.dev.toString() + ':' + writtenIdentity.ino.toString()); if (stateSha256(fs.readFileSync(destination)) !== entry.fingerprint) { throw new StateMigrationError('Staged legacy file failed fingerprint verification: ' + entry.path); } From 803c0c1968fa06ffda0b250522dad2b64cbe9c0f Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:27:17 +0200 Subject: [PATCH 073/177] docs(T04): record W73-28 RED/GREEN CI and exact v0.13 port provenance --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index 9e755f2a4..c3ebb2c79 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **W73-28 staged restore path alias hardening (30 October 2026):** draft, unmerged diagnostic [PR #116](https://github.com/eybersjp/development-kit/pull/116) introduced a signed-backup regression using distinct `s.json` and Unicode U+017F long-s paths. Existing lowercase-only normalized destination preflight silently accepted the pair on Ubuntu, reproduced as `Missing expected exception` in test-only [CI #41079128579](https://github.com/eybersjp/development-kit/actions/runs/41079128579) and [#41079372395](https://github.com/eybersjp/development-kit/actions/runs/41079372395) (RED). The narrow correction conservatively checks uppercase-folded destinations before staging and checks staged file dev/inode identities before replacing an existing destination, addressing additional NTFS 8.3/volume-specific aliases where textual folding is insufficient. Diagnostic SHA `d849880ece9a58f7bf9f56feab02882c028811c9` passed both full Ubuntu and Windows v0.13 validation lanes in [CI #41079431166](https://github.com/eybersjp/development-kit/actions/runs/41079431166) (GREEN). Only test and runtime files were ported to actual integration PR #92 in `5920cd352dae245d6360a59fd62672c1259bccb9` and `ea32b2bbff336f6dde4f581c2315b58f4a7e86b0`. The real NTFS 8.3 alias variant and sudden physical power-loss durability still require independent evidence; this test is a conservative cross-platform rejection control, **not** physical NTFS certification or T04 ACCEPTED. PR #116 must not merge. + - **T04-P2 pending suffix validation before recovery mutation (30 October 2026):** independent code review on PR #92 SHA `73603f8a2004100b0fb8e302f89825eecc5638b5` found that `recoverPendingCommitLocked()` could truncate/append/fsync canonical bytes before checking a hash-invalid pending event or conflicting snapshot witness. Isolated draft diagnostic [PR #117](https://github.com/eybersjp/development-kit/pull/117) test-only `5d02b5d03b46f9e24a637f4a1696fc9e407ab20c` reproduced evidence destruction with both zero-byte and partially appended suffix fixtures in Ubuntu [CI #41079260840](https://github.com/eybersjp/development-kit/actions/runs/41079260840) (RED). Narrow source correction moves full proposed-ledger hash-chain validation and snapshot-witness checks before any canonical mutation; diagnostic SHA `e884d88fb3a11c59acc77f159dea209de4cd730e` passed both complete Ubuntu and Windows v0.13 lanes in [CI #41079450641](https://github.com/eybersjp/development-kit/actions/runs/41079450641). Ported only validated runtime/test to actual PR #92 in `385f7c9edc3fd2e882edcc31589f6c20de467124` and `130bb5c7ff05a2762bbf0a20b3ba2f444742e04a`; new combined-head CI and exact-source re-review remain mandatory. PR #117 remains draft, unmerged. The other new P2 review finding is stale `/v1/scale-context` API projection after V2 cutover, which needs a separate compatibility correction without reintroducing legacy `current.json` for fresh V2 projects. No physical NTFS power-loss result or independent risk-3 ACCEPTED verdict is claimed. - **T04-P2 composite index identity collision (30 October 2026):** actual integration PR #92 review identified a collision between valid identities (`a::b`, `c`) and (`a`, `b::c`), leaking records across contract/task/run/lifecycle filters. Isolated draft diagnostic [PR #114](https://github.com/eybersjp/development-kit/pull/114) reproduced the defect with test-only SHA `85a97cfc55e17c3ef36c1364619330f04b73cf0c`: Ubuntu and Windows CI [#41074686949](https://github.com/eybersjp/development-kit/actions/runs/41074686949) RED. Follow-up stale-index test `cfe3656dd95a0cce68d73734171350757d60799d` reproduced a second failure on Ubuntu in [#41074990296](https://github.com/eybersjp/development-kit/actions/runs/41074990296): a legacy derived index with matching source hash was still trusted. Diagnostic SHA `dbba5619b783f8c2b5451f24766410847728b4e2` passed both complete Ubuntu/Windows v0.13 CI lanes [#41075160424](https://github.com/eybersjp/development-kit/actions/runs/41075160424). The narrow correction uses collision-free JSON tuple keys and compares loaded disposable indexes to the canonical rebuilt index, rebuilding stale mappings before serving queries. Ported only the verified runtime/test changes to actual PR #92 as `57744d530ee38d457dc945d01982c08cd4cacd83` and `8dff8e451ab3cbebdefec7d9daeca8707112ccd2`; production source/test SHA `8dff8e451ab3cbebdefec7d9daeca8707112ccd2` passed both full Ubuntu/Windows v0.13 jobs in [CI #41076212580](https://github.com/eybersjp/development-kit/actions/runs/41076212580); the later combined head requires its own fresh CI. PR #114 remains draft, never merge. This is not T04 ACCEPTED, does not resolve Windows physical power-loss evidence or independent risk-3 reviews. From 4fe2fb7ab28a3c05d5b248e9dbdaf68b7b7567a2 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:37:18 +0200 Subject: [PATCH 074/177] fix(v0.13 T04): port CI-verified authority-aware Scale & Context API projection --- runtime/api/runtime-api-service.mjs | 76 ++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/runtime/api/runtime-api-service.mjs b/runtime/api/runtime-api-service.mjs index 7e3bf5cd8..7fcb448db 100644 --- a/runtime/api/runtime-api-service.mjs +++ b/runtime/api/runtime-api-service.mjs @@ -22,6 +22,7 @@ import { resolveMemoryIdentity } from '../intelligence/memory-identity.mjs'; import { resolveEffectiveSettings } from '../intelligence/settings.mjs'; import { LocalMemoryProvider } from '../intelligence/local-memory-provider.mjs'; import { getCurrentState } from '../autopilot/state-store.mjs'; +import { isStateEngineV2Active } from '../orchestration/state-engine-v2.mjs'; import { validateWorkflowState } from '../autopilot/validators.mjs'; import { loadWorkspaceTargetRegistry } from '../orchestration/workspace-targets.mjs'; import { loadCostRecords } from '../orchestration/cost-observatory.mjs'; @@ -273,38 +274,66 @@ export class RuntimeApiService { } if (method === 'GET' && pathname === '/v1/scale-context') { - // Read-only projection over v0.11's supported state authority. T04's - // canonical event ledger is deliberately NOT included in v0.12. + // Select the project's actual persistence authority. Released v0.12.1 + // projects retain the legacy pointer projection; fresh/migrated v0.13 + // projects must never read a stale pointer or fabricate one for the UI. const directory = path.join(this.rootDir, '.development-kit', 'autopilot', 'state'); const pointerPath = path.join(directory, 'current.json'); let stateHealth = 'unavailable'; let state = { active: false, currentStage: null, revision: null }; let activeContractId = null; - if (fs.existsSync(pointerPath)) { + let v2Active = false; + let v2Invalid = false; + try { + v2Active = isStateEngineV2Active(this.rootDir); + } catch { + v2Invalid = true; + } + if (v2Active) { try { - const pointer = JSON.parse(fs.readFileSync(pointerPath, 'utf8')); - if (!Number.isSafeInteger(pointer.currentRevision) || - !/^revision-\d{6}\.json$/.test(pointer.currentRevisionFile) || - Number(pointer.currentRevisionFile.slice(9, 15)) !== pointer.currentRevision) { - throw new Error('Invalid revision pointer'); - } - const revisionPath = path.join(directory, pointer.currentRevisionFile); - if (!fs.existsSync(revisionPath)) { - stateHealth = 'recovery-required'; - } else { - const revision = JSON.parse(fs.readFileSync(revisionPath, 'utf8')); + const revision = getCurrentState(this.rootDir); + if (revision) { validateWorkflowState(revision); - if (revision.stateRevision !== pointer.currentRevision) { - throw new Error('Pointer revision mismatch'); - } stateHealth = 'unverified'; - state = { active: true, currentStage: revision.currentStage, revision: revision.stateRevision }; + state = { active: true, currentStage: revision.currentStage, + revision: revision.stateRevision }; activeContractId = typeof revision.orchestration?.activeContractId === 'string' ? revision.orchestration.activeContractId : null; } } catch { stateHealth = 'degraded'; } + } else if (v2Invalid) { + // Never silently fall back to a frozen legacy pointer when canonical + // V2 metadata is present but invalid or cannot be trusted. + stateHealth = 'degraded'; + } else { + if (fs.existsSync(pointerPath)) { + try { + const pointer = JSON.parse(fs.readFileSync(pointerPath, 'utf8')); + if (!Number.isSafeInteger(pointer.currentRevision) || + !/^revision-\d{6}\.json$/.test(pointer.currentRevisionFile) || + Number(pointer.currentRevisionFile.slice(9, 15)) !== pointer.currentRevision) { + throw new Error('Invalid revision pointer'); + } + const revisionPath = path.join(directory, pointer.currentRevisionFile); + if (!fs.existsSync(revisionPath)) { + stateHealth = 'recovery-required'; + } else { + const revision = JSON.parse(fs.readFileSync(revisionPath, 'utf8')); + validateWorkflowState(revision); + if (revision.stateRevision !== pointer.currentRevision) { + throw new Error('Pointer revision mismatch'); + } + stateHealth = 'unverified'; + state = { active: true, currentStage: revision.currentStage, revision: revision.stateRevision }; + activeContractId = typeof revision.orchestration?.activeContractId === 'string' + ? revision.orchestration.activeContractId : null; + } + } catch { + stateHealth = 'degraded'; + } + } } const modules = { workspaceTargets: 'workspace-targets.mjs', @@ -319,9 +348,11 @@ export class RuntimeApiService { id, { installed: fs.existsSync(new URL('../orchestration/' + filename, import.meta.url)), accepted: null, acceptanceStatus: 'not-independently-established' }, ])); - capabilities.stateEngineV2 = { - installed: false, accepted: false, acceptanceStatus: 'deferred-to-v0.13.0', - }; + capabilities.stateEngineV2 = v2Active + ? { installed: true, accepted: null, acceptanceStatus: 'not-independently-established' } + : v2Invalid + ? { installed: false, accepted: null, acceptanceStatus: 'degraded' } + : { installed: false, accepted: false, acceptanceStatus: 'deferred-to-v0.13.0' }; // Operational state is observed strictly from the existing stable // project stores. Never confuse an installed module with an active, // verified, or accepted capability, and never mutate data on GET. @@ -384,7 +415,8 @@ export class RuntimeApiService { }; return this._json(res, 200, { schemaVersion: '1.0.0', - canonicalStateStore: 'autopilot-versioned-snapshots', + canonicalStateStore: v2Active || v2Invalid + ? 'state-engine-v2-event-ledger' : 'autopilot-versioned-snapshots', stateHealth, state, capabilities, From 00d5628beee97a5d1131ba1f726517530c51d2a5 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:37:26 +0200 Subject: [PATCH 075/177] test(v0.13 T04): port RED/GREEN Control Center V2 projection compatibility regression --- scripts/state-engine-v013.test.mjs | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scripts/state-engine-v013.test.mjs b/scripts/state-engine-v013.test.mjs index 0545794ef..eba5fc6de 100644 --- a/scripts/state-engine-v013.test.mjs +++ b/scripts/state-engine-v013.test.mjs @@ -24,6 +24,7 @@ import { } from '../runtime/orchestration/state-engine-migration.mjs'; import { createInitialState } from '../runtime/autopilot/transition-model.mjs'; import { saveStateRevision } from '../runtime/autopilot/state-store.mjs'; +import { RuntimeApiService } from '../runtime/api/runtime-api-service.mjs'; import { persistRunManifest, persistRunStateRevision, updateRun } from '../runtime/orchestration/orchestration-run.mjs'; function tempDir(t, prefix = 'dk-v013-state-') { @@ -801,3 +802,36 @@ test('W73-24 staged confidential child is never copied under a broader root DACL assert.equal(dacl(path.join(targetRoot, 'confidential.txt'), 'File'), childAcl, 'Published confidential child must preserve its more restrictive DACL'); }); + +test('V13-API-PROJECTION-01 scale-context observes canonical V2 workflow without a legacy pointer', async (t) => { + const rootDir = tempDir(t, 'dk-v013-control-center-v2-'); + const state = createInitialState({ autonomy: 'guided-autopilot' }, rootDir); + saveStateRevision(state, rootDir); + const legacyPointer = path.join(rootDir, '.development-kit', 'autopilot', 'state', 'current.json'); + assert.equal(fs.existsSync(legacyPointer), false, + 'Fresh V2 projects must not fabricate a legacy pointer for UI compatibility'); + const service = new RuntimeApiService({ rootDir }); + const info = await service.start(); + try { + const get = async route => { + const response = await fetch(info.url + route); + assert.equal(response.status, 200, route); + return response.json(); + }; + const workflow = await get('/v1/workflow'); + assert.equal(workflow.state?.workflowId, state.workflowId); + const projection = await get('/v1/scale-context'); + assert.equal(projection.state.active, true, + 'Scale & Context must not report inactive while canonical V2 workflow is active'); + assert.equal(projection.state.currentStage, state.currentStage); + assert.equal(projection.state.revision, state.stateRevision); + assert.equal(projection.canonicalStateStore, 'state-engine-v2-event-ledger'); + assert.equal(projection.capabilities.stateEngineV2.installed, true); + assert.equal(projection.capabilities.stateEngineV2.accepted, null, + 'Installed and active is not independently ACCEPTED'); + assert.equal(fs.existsSync(legacyPointer), false, + 'Read-only projection must not create or mutate legacy authority'); + } finally { + await service.stop(); + } +}); From e0b381d3861a81db7d8c866e83a28e8cd184baaa Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:37:33 +0200 Subject: [PATCH 076/177] docs(contract): authorize v0.13 T04 Runtime API canonical projection compatibility scope --- scripts/v013-state-engine-v2-contract.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/v013-state-engine-v2-contract.mjs b/scripts/v013-state-engine-v2-contract.mjs index e4d10a45f..5a9f9ad16 100644 --- a/scripts/v013-state-engine-v2-contract.mjs +++ b/scripts/v013-state-engine-v2-contract.mjs @@ -22,6 +22,7 @@ const task = { 'runtime/orchestration/state-engine-v2.mjs', 'runtime/orchestration/state-engine-migration.mjs', 'runtime/autopilot/state-store.mjs', + 'runtime/api/runtime-api-service.mjs', 'runtime/orchestration/orchestration-run.mjs', 'runtime/orchestration/index.mjs', 'schemas/state-engine-*.schema.json', From 952ae0dec1ae7924286c37d3567ec571689efea7 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 17:37:44 +0200 Subject: [PATCH 077/177] docs(memory): record v0.13 T04 Control Center projection RED/GREEN and source-bound port --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index c3ebb2c79..1d154b8a9 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-P2 v0.13 Control Center API projection compatibility (30 October 2026):** exact-source independent review of PR #92 found that `/v1/workflow` reads canonical V2 while `/v1/scale-context` still reads stale v0.12 legacy `current.json`, reporting an active fresh workflow as inactive and a migrated workflow as frozen. Isolated draft diagnostic [PR #118](https://github.com/eybersjp/development-kit/pull/118) test-only `f9a1972ccfcc5e7b2d580612ef9a94b63627aa46` reproduced the mismatch in Ubuntu [CI #41080813268](https://github.com/eybersjp/development-kit/actions/runs/41080813268) (RED). A narrow read-only authority-aware API projection prefers a verified active V2 layout, refuses stale legacy fallback when V2 metadata is invalid, preserves v0.12 legacy behavior for non-V2 projects, never creates a legacy pointer, and reports V2 as installed but **not independently accepted**. Explicit v0.13 contract scope expanded to `runtime/api/runtime-api-service.mjs`. Diagnostic SHA `85b8628fcc8ecc0b47eb7fa4d1a22c19e3996932` passed **both complete Ubuntu and Windows v0.13 validation jobs** in [CI #41081063299](https://github.com/eybersjp/development-kit/actions/runs/41081063299). Ported only validated API source, regression and scope to actual PR #92 in `4fe2fb7ab28a3c05d5b248e9dbdaf68b7b7567a2`, `00d5628beee97a5d1131ba1f726517530c51d2a5`, `e0b381d3861a81db7d8c866e83a28e8cd184baaa`. PR #118 remains draft, unmerged; the new exact integration head requires fresh CI and independent source-bound re-review. No physical Windows NTFS power-loss certification, risk-3 ACCEPTED verdict, mainline merge or release is claimed. + - **W73-28 staged restore path alias hardening (30 October 2026):** draft, unmerged diagnostic [PR #116](https://github.com/eybersjp/development-kit/pull/116) introduced a signed-backup regression using distinct `s.json` and Unicode U+017F long-s paths. Existing lowercase-only normalized destination preflight silently accepted the pair on Ubuntu, reproduced as `Missing expected exception` in test-only [CI #41079128579](https://github.com/eybersjp/development-kit/actions/runs/41079128579) and [#41079372395](https://github.com/eybersjp/development-kit/actions/runs/41079372395) (RED). The narrow correction conservatively checks uppercase-folded destinations before staging and checks staged file dev/inode identities before replacing an existing destination, addressing additional NTFS 8.3/volume-specific aliases where textual folding is insufficient. Diagnostic SHA `d849880ece9a58f7bf9f56feab02882c028811c9` passed both full Ubuntu and Windows v0.13 validation lanes in [CI #41079431166](https://github.com/eybersjp/development-kit/actions/runs/41079431166) (GREEN). Only test and runtime files were ported to actual integration PR #92 in `5920cd352dae245d6360a59fd62672c1259bccb9` and `ea32b2bbff336f6dde4f581c2315b58f4a7e86b0`. The real NTFS 8.3 alias variant and sudden physical power-loss durability still require independent evidence; this test is a conservative cross-platform rejection control, **not** physical NTFS certification or T04 ACCEPTED. PR #116 must not merge. - **T04-P2 pending suffix validation before recovery mutation (30 October 2026):** independent code review on PR #92 SHA `73603f8a2004100b0fb8e302f89825eecc5638b5` found that `recoverPendingCommitLocked()` could truncate/append/fsync canonical bytes before checking a hash-invalid pending event or conflicting snapshot witness. Isolated draft diagnostic [PR #117](https://github.com/eybersjp/development-kit/pull/117) test-only `5d02b5d03b46f9e24a637f4a1696fc9e407ab20c` reproduced evidence destruction with both zero-byte and partially appended suffix fixtures in Ubuntu [CI #41079260840](https://github.com/eybersjp/development-kit/actions/runs/41079260840) (RED). Narrow source correction moves full proposed-ledger hash-chain validation and snapshot-witness checks before any canonical mutation; diagnostic SHA `e884d88fb3a11c59acc77f159dea209de4cd730e` passed both complete Ubuntu and Windows v0.13 lanes in [CI #41079450641](https://github.com/eybersjp/development-kit/actions/runs/41079450641). Ported only validated runtime/test to actual PR #92 in `385f7c9edc3fd2e882edcc31589f6c20de467124` and `130bb5c7ff05a2762bbf0a20b3ba2f444742e04a`; new combined-head CI and exact-source re-review remain mandatory. PR #117 remains draft, unmerged. The other new P2 review finding is stale `/v1/scale-context` API projection after V2 cutover, which needs a separate compatibility correction without reintroducing legacy `current.json` for fresh V2 projects. No physical NTFS power-loss result or independent risk-3 ACCEPTED verdict is claimed. From f35455dff057cd550eccc4d0100f54292b65d711 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 18:24:34 +0200 Subject: [PATCH 078/177] fix(v0.13 T04): permit literal JSON keys in internal patch paths (PR #119 verified) --- runtime/orchestration/state-engine-v2.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 2580f314e..906f3b140 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -913,8 +913,8 @@ export function loadCanonicalEvents(rootDir = process.cwd()) { } function normalizePathSegments(value) { - if (!Array.isArray(value) || value.some((segment) => typeof segment !== 'string' || !segment || /[\r\n\0]/.test(segment))) { - throw new StateEngineError('Patch operation path must be a non-empty string array'); + if (!Array.isArray(value) || value.some((segment) => typeof segment !== 'string')) { + throw new StateEngineError('Patch operation path must be a string array'); } return value; } From b6a25cbc510dd8011fc43319dc7927f71b2b42c2 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 18:24:42 +0200 Subject: [PATCH 079/177] test(v0.13 T04): retain verified JSON-key update/deletion regression (PR #119) --- scripts/state-engine-v2.test.mjs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/state-engine-v2.test.mjs b/scripts/state-engine-v2.test.mjs index bafd44c8a..4e43e0669 100644 --- a/scripts/state-engine-v2.test.mjs +++ b/scripts/state-engine-v2.test.mjs @@ -579,3 +579,24 @@ test('V13-CONC-BOOTSTRAP-04 live initializer staging links are not reclaimed', ( } assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); }); + +test('T04-P2 JSON object keys including empty, control and Unicode strings survive patch update and deletion', (t) => { + for (const [index, key] of ['', '\n', '\r', '\u0000', 'x\ny', 'é', '__proto__'].entries()) { + const rootDir = tempProject(t, 'dk-v13-patch-key-'); + const entityId = 'literal-key-' + index; + const first = { stateRevision: 1, metadata: { [key]: { count: 1 } } }; + const second = { stateRevision: 2, metadata: { [key]: { count: 2 } } }; + const third = { stateRevision: 3, metadata: {} }; + appendEntityState({ rootDir, entityType: 'generic-state', entityId, + state: first, actorClass: 'regression' }); + appendEntityState({ rootDir, entityType: 'generic-state', entityId, + state: second, actorClass: 'regression' }); + assert.deepEqual(loadStateSnapshot(rootDir, { rebuildIfNeeded: true }) + .entities['generic-state'][entityId].state, second, 'update key ' + JSON.stringify(key)); + appendEntityState({ rootDir, entityType: 'generic-state', entityId, + state: third, actorClass: 'regression' }); + assert.deepEqual(rebuildSnapshotFromEvents(loadCanonicalEvents(rootDir)) + .entities['generic-state'][entityId].state, third, 'delete key ' + JSON.stringify(key)); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); + } +}); From fc1d6715d95cd530ed17fc2b3f1ae801269b0854 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 18:30:05 +0200 Subject: [PATCH 080/177] fix(v0.13 T04): keep read-only projection from repairing unmigrated legacy state (PR #120 verified) --- runtime/api/runtime-api-service.mjs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/runtime/api/runtime-api-service.mjs b/runtime/api/runtime-api-service.mjs index 7fcb448db..c9db273e7 100644 --- a/runtime/api/runtime-api-service.mjs +++ b/runtime/api/runtime-api-service.mjs @@ -22,7 +22,7 @@ import { resolveMemoryIdentity } from '../intelligence/memory-identity.mjs'; import { resolveEffectiveSettings } from '../intelligence/settings.mjs'; import { LocalMemoryProvider } from '../intelligence/local-memory-provider.mjs'; import { getCurrentState } from '../autopilot/state-store.mjs'; -import { isStateEngineV2Active } from '../orchestration/state-engine-v2.mjs'; +import { isStateEngineV2Active, hasCompletedStateMigration, loadLatestEntityState } from '../orchestration/state-engine-v2.mjs'; import { validateWorkflowState } from '../autopilot/validators.mjs'; import { loadWorkspaceTargetRegistry } from '../orchestration/workspace-targets.mjs'; import { loadCostRecords } from '../orchestration/cost-observatory.mjs'; @@ -289,7 +289,26 @@ export class RuntimeApiService { } catch { v2Invalid = true; } + // V2 layout existence alone does not migrate an existing legacy workflow. + // Selecting legacy authority must use the non-mutating pointer projection + // below: getCurrentState() can repair current.json as a side effect. + let v2WorkflowAuthority = v2Active; if (v2Active) { + try { + const hasLegacyRevisions = fs.existsSync(directory) && + fs.readdirSync(directory).some((name) => /^revision-\d{6}\.json$/.test(name)); + if (hasLegacyRevisions) { + v2WorkflowAuthority = hasCompletedStateMigration(this.rootDir) && + loadLatestEntityState('autopilot-workflow', this.rootDir) !== null; + } + } catch { + // An unreadable V2 migration witness cannot authorize fallback. + v2Active = false; + v2Invalid = true; + v2WorkflowAuthority = false; + } + } + if (v2WorkflowAuthority) { try { const revision = getCurrentState(this.rootDir); if (revision) { From a28b3cce36c57323a9cabc8d291ed1cf1a26a951 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 18:30:17 +0200 Subject: [PATCH 081/177] test(v0.13 T04): preserve corrupt legacy pointer and unmigrated workflow projection regressions --- scripts/state-engine-v013.test.mjs | 58 ++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/scripts/state-engine-v013.test.mjs b/scripts/state-engine-v013.test.mjs index eba5fc6de..37ae4cca1 100644 --- a/scripts/state-engine-v013.test.mjs +++ b/scripts/state-engine-v013.test.mjs @@ -835,3 +835,61 @@ test('V13-API-PROJECTION-01 scale-context observes canonical V2 workflow without await service.stop(); } }); + +test('V13-API-PROJECTION-02 read-only projection never repairs unmigrated legacy pointer under V2 metadata', async (t) => { + const rootDir = tempDir(t, 'dk-v013-api-legacy-pointer-'); + const legacyDir = path.join(rootDir, '.development-kit', 'autopilot', 'state'); + fs.mkdirSync(legacyDir, { recursive: true }); + const legacy = createInitialState({ autonomy: 'guided-autopilot' }, rootDir); + const revisionFile = 'revision-' + String(legacy.stateRevision).padStart(6, '0') + '.json'; + fs.writeFileSync(path.join(legacyDir, revisionFile), JSON.stringify(legacy)); + const pointerPath = path.join(legacyDir, 'current.json'); + const invalidPointer = '{"invalid":'; + fs.writeFileSync(pointerPath, invalidPointer); + appendMetadataEvent({ rootDir, eventType: 'UNRELATED_METADATA', payload: { note: 'unmigrated' } }); + const paths = getStateEnginePaths(rootDir); + const originalEvents = fs.readFileSync(paths.events, 'utf8'); + + const service = new RuntimeApiService({ rootDir }); + const info = await service.start(); + try { + const response = await fetch(info.url + '/v1/scale-context'); + assert.equal(response.status, 200); + const projection = await response.json(); + assert.equal(projection.stateHealth, 'degraded', + 'Corrupt legacy pointer must be reported, not silently repaired'); + assert.equal(projection.state.active, false); + assert.equal(fs.readFileSync(pointerPath, 'utf8'), invalidPointer, + 'Read-only projection must preserve the exact corrupt pointer evidence'); + assert.equal(fs.readFileSync(paths.events, 'utf8'), originalEvents, + 'Read-only projection must not modify canonical V2 events'); + } finally { + await service.stop(); + } +}); + +test('V13-API-PROJECTION-03 V2 metadata alone does not hide valid unmigrated legacy workflow', async (t) => { + const rootDir = tempDir(t, 'dk-v013-api-legacy-valid-'); + const legacyDir = path.join(rootDir, '.development-kit', 'autopilot', 'state'); + fs.mkdirSync(legacyDir, { recursive: true }); + const legacy = createInitialState({ autonomy: 'guided-autopilot' }, rootDir); + const revisionFile = 'revision-' + String(legacy.stateRevision).padStart(6, '0') + '.json'; + fs.writeFileSync(path.join(legacyDir, revisionFile), JSON.stringify(legacy)); + const pointerPath = path.join(legacyDir, 'current.json'); + const pointer = JSON.stringify({ currentRevision: legacy.stateRevision, + currentRevisionFile: revisionFile, workflowId: legacy.workflowId }); + fs.writeFileSync(pointerPath, pointer); + appendMetadataEvent({ rootDir, eventType: 'UNRELATED_METADATA', payload: { note: 'unmigrated' } }); + const service = new RuntimeApiService({ rootDir }); + const info = await service.start(); + try { + const response = await fetch(info.url + '/v1/scale-context'); + assert.equal(response.status, 200); + const projection = await response.json(); + assert.equal(projection.state.active, true); + assert.equal(projection.state.revision, legacy.stateRevision); + assert.equal(fs.readFileSync(pointerPath, 'utf8'), pointer); + } finally { + await service.stop(); + } +}); From 66201a97f7af979f926b0fd40d459c26620bfb67 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 18:30:40 +0200 Subject: [PATCH 082/177] docs(memory): record test-first T04 P2 JSON-key and read-only legacy API CI evidence --- memory.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memory.md b/memory.md index 1d154b8a9..3860efe4d 100644 --- a/memory.md +++ b/memory.md @@ -11,7 +11,7 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) -- **T04-P2 v0.13 Control Center API projection compatibility (30 October 2026):** exact-source independent review of PR #92 found that `/v1/workflow` reads canonical V2 while `/v1/scale-context` still reads stale v0.12 legacy `current.json`, reporting an active fresh workflow as inactive and a migrated workflow as frozen. Isolated draft diagnostic [PR #118](https://github.com/eybersjp/development-kit/pull/118) test-only `f9a1972ccfcc5e7b2d580612ef9a94b63627aa46` reproduced the mismatch in Ubuntu [CI #41080813268](https://github.com/eybersjp/development-kit/actions/runs/41080813268) (RED). A narrow read-only authority-aware API projection prefers a verified active V2 layout, refuses stale legacy fallback when V2 metadata is invalid, preserves v0.12 legacy behavior for non-V2 projects, never creates a legacy pointer, and reports V2 as installed but **not independently accepted**. Explicit v0.13 contract scope expanded to `runtime/api/runtime-api-service.mjs`. Diagnostic SHA `85b8628fcc8ecc0b47eb7fa4d1a22c19e3996932` passed **both complete Ubuntu and Windows v0.13 validation jobs** in [CI #41081063299](https://github.com/eybersjp/development-kit/actions/runs/41081063299). Ported only validated API source, regression and scope to actual PR #92 in `4fe2fb7ab28a3c05d5b248e9dbdaf68b7b7567a2`, `00d5628beee97a5d1131ba1f726517530c51d2a5`, `e0b381d3861a81db7d8c866e83a28e8cd184baaa`. PR #118 remains draft, unmerged; the new exact integration head requires fresh CI and independent source-bound re-review. No physical Windows NTFS power-loss certification, risk-3 ACCEPTED verdict, mainline merge or release is claimed. +- **T04-P2 literal JSON patch-key compatibility (30 October 2026):** PR #92 exact-source review on `952ae0dec1ae7924286c37d3567ec571689efea7` identified that internally generated patch paths rejected valid JSON keys including empty strings, LF, CR and NUL. Isolated **draft, do-not-merge** diagnostic [PR #119](https://github.com/eybersjp/development-kit/pull/119) reproduced the defect with adversarial update/deletion tests on **both Ubuntu and Windows** in [CI #41088497647](https://github.com/eybersjp/development-kit/actions/runs/41088497647) (RED). The narrow correction permits any literal string segment in JSON array paths while retaining type validation and own-property-safe patch writes. Diagnostic `51f80f691d485e47dd201a42a3ae64545d1771fb` passed **both full Ubuntu/Windows v0.13 validation jobs** in [CI #41088653146](https://github.com/eybersjp/development-kit/actions/runs/41088653146) (GREEN). Only runtime and test were ported to actual v0.13 PR #92 in `f35455dff057cd550eccc4d0100f54292b65d711` and `b6a25cbc510dd8011fc43319dc7927f71b2b42c2`; diagnostic workflow exception was **not** ported. This does not establish independent acceptance or NTFS physical power-loss durability.\n\n- **T04-P2 non-mutating legacy Control Center projection (30 October 2026):** PR #92 exact-source review identified that a V2 metadata-only ledger plus unmigrated legacy workflow could cause read-only `GET /v1/scale-context` to repair a corrupt `current.json` pointer. Isolated **draft, do-not-merge** diagnostic [PR #120](https://github.com/eybersjp/development-kit/pull/120) reproduced unauthorized pointer repair and tested a valid unmigrated legacy pointer; **Ubuntu and Windows** [CI #41088803827](https://github.com/eybersjp/development-kit/actions/runs/41088803827) RED. Narrow correction distinguishes active V2 workflow authority from V2 metadata presence; unmigrated legacy workflows use the existing non-mutating pointer projection, and unreadable V2 migration evidence fails closed rather than authorizing fallback. Diagnostic `b9374618c054c1fd9b596b68b43ac402d954bf01` passed **both full Ubuntu/Windows v0.13 validation jobs** in [CI #41089162418](https://github.com/eybersjp/development-kit/actions/runs/41089162418) (GREEN). Only runtime/test were ported to actual v0.13 PR #92 in `fc1d6715d95cd530ed17fc2b3f1ae801269b0854` and `a28b3cce36c57323a9cabc8d291ed1cf1a26a951`. This does not establish independent acceptance or physical Windows power-loss assurance.\n\n- **Acceptance boundary after both fixes:** actual PR #92 must complete fresh full Ubuntu/Windows CI on the **combined final head**, two distinct complete runs on the same immutable reviewed SHA, genuine distinct authenticated code/architecture/security reviews with all major threads resolved, controlled physical NTFS sudden-power-loss evidence for Issue #73, and persisted deterministic risk-3 `ACCEPTED`. Neither diagnostic PR #119/#120 nor historical PR #70 is authorized to merge; no release or approval is claimed.\n\n- **T04-P2 v0.13 Control Center API projection compatibility (30 October 2026):** exact-source independent review of PR #92 found that `/v1/workflow` reads canonical V2 while `/v1/scale-context` still reads stale v0.12 legacy `current.json`, reporting an active fresh workflow as inactive and a migrated workflow as frozen. Isolated draft diagnostic [PR #118](https://github.com/eybersjp/development-kit/pull/118) test-only `f9a1972ccfcc5e7b2d580612ef9a94b63627aa46` reproduced the mismatch in Ubuntu [CI #41080813268](https://github.com/eybersjp/development-kit/actions/runs/41080813268) (RED). A narrow read-only authority-aware API projection prefers a verified active V2 layout, refuses stale legacy fallback when V2 metadata is invalid, preserves v0.12 legacy behavior for non-V2 projects, never creates a legacy pointer, and reports V2 as installed but **not independently accepted**. Explicit v0.13 contract scope expanded to `runtime/api/runtime-api-service.mjs`. Diagnostic SHA `85b8628fcc8ecc0b47eb7fa4d1a22c19e3996932` passed **both complete Ubuntu and Windows v0.13 validation jobs** in [CI #41081063299](https://github.com/eybersjp/development-kit/actions/runs/41081063299). Ported only validated API source, regression and scope to actual PR #92 in `4fe2fb7ab28a3c05d5b248e9dbdaf68b7b7567a2`, `00d5628beee97a5d1131ba1f726517530c51d2a5`, `e0b381d3861a81db7d8c866e83a28e8cd184baaa`. PR #118 remains draft, unmerged; the new exact integration head requires fresh CI and independent source-bound re-review. No physical Windows NTFS power-loss certification, risk-3 ACCEPTED verdict, mainline merge or release is claimed. - **W73-28 staged restore path alias hardening (30 October 2026):** draft, unmerged diagnostic [PR #116](https://github.com/eybersjp/development-kit/pull/116) introduced a signed-backup regression using distinct `s.json` and Unicode U+017F long-s paths. Existing lowercase-only normalized destination preflight silently accepted the pair on Ubuntu, reproduced as `Missing expected exception` in test-only [CI #41079128579](https://github.com/eybersjp/development-kit/actions/runs/41079128579) and [#41079372395](https://github.com/eybersjp/development-kit/actions/runs/41079372395) (RED). The narrow correction conservatively checks uppercase-folded destinations before staging and checks staged file dev/inode identities before replacing an existing destination, addressing additional NTFS 8.3/volume-specific aliases where textual folding is insufficient. Diagnostic SHA `d849880ece9a58f7bf9f56feab02882c028811c9` passed both full Ubuntu and Windows v0.13 validation lanes in [CI #41079431166](https://github.com/eybersjp/development-kit/actions/runs/41079431166) (GREEN). Only test and runtime files were ported to actual integration PR #92 in `5920cd352dae245d6360a59fd62672c1259bccb9` and `ea32b2bbff336f6dde4f581c2315b58f4a7e86b0`. The real NTFS 8.3 alias variant and sudden physical power-loss durability still require independent evidence; this test is a conservative cross-platform rejection control, **not** physical NTFS certification or T04 ACCEPTED. PR #116 must not merge. From bbe7f6342be96757c9089d2ef29ffcdec0c6338e Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 18:30:51 +0200 Subject: [PATCH 083/177] docs(memory): render T04 test-first evidence as separate Markdown entries --- memory.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/memory.md b/memory.md index 3860efe4d..478b2aa00 100644 --- a/memory.md +++ b/memory.md @@ -11,7 +11,13 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) -- **T04-P2 literal JSON patch-key compatibility (30 October 2026):** PR #92 exact-source review on `952ae0dec1ae7924286c37d3567ec571689efea7` identified that internally generated patch paths rejected valid JSON keys including empty strings, LF, CR and NUL. Isolated **draft, do-not-merge** diagnostic [PR #119](https://github.com/eybersjp/development-kit/pull/119) reproduced the defect with adversarial update/deletion tests on **both Ubuntu and Windows** in [CI #41088497647](https://github.com/eybersjp/development-kit/actions/runs/41088497647) (RED). The narrow correction permits any literal string segment in JSON array paths while retaining type validation and own-property-safe patch writes. Diagnostic `51f80f691d485e47dd201a42a3ae64545d1771fb` passed **both full Ubuntu/Windows v0.13 validation jobs** in [CI #41088653146](https://github.com/eybersjp/development-kit/actions/runs/41088653146) (GREEN). Only runtime and test were ported to actual v0.13 PR #92 in `f35455dff057cd550eccc4d0100f54292b65d711` and `b6a25cbc510dd8011fc43319dc7927f71b2b42c2`; diagnostic workflow exception was **not** ported. This does not establish independent acceptance or NTFS physical power-loss durability.\n\n- **T04-P2 non-mutating legacy Control Center projection (30 October 2026):** PR #92 exact-source review identified that a V2 metadata-only ledger plus unmigrated legacy workflow could cause read-only `GET /v1/scale-context` to repair a corrupt `current.json` pointer. Isolated **draft, do-not-merge** diagnostic [PR #120](https://github.com/eybersjp/development-kit/pull/120) reproduced unauthorized pointer repair and tested a valid unmigrated legacy pointer; **Ubuntu and Windows** [CI #41088803827](https://github.com/eybersjp/development-kit/actions/runs/41088803827) RED. Narrow correction distinguishes active V2 workflow authority from V2 metadata presence; unmigrated legacy workflows use the existing non-mutating pointer projection, and unreadable V2 migration evidence fails closed rather than authorizing fallback. Diagnostic `b9374618c054c1fd9b596b68b43ac402d954bf01` passed **both full Ubuntu/Windows v0.13 validation jobs** in [CI #41089162418](https://github.com/eybersjp/development-kit/actions/runs/41089162418) (GREEN). Only runtime/test were ported to actual v0.13 PR #92 in `fc1d6715d95cd530ed17fc2b3f1ae801269b0854` and `a28b3cce36c57323a9cabc8d291ed1cf1a26a951`. This does not establish independent acceptance or physical Windows power-loss assurance.\n\n- **Acceptance boundary after both fixes:** actual PR #92 must complete fresh full Ubuntu/Windows CI on the **combined final head**, two distinct complete runs on the same immutable reviewed SHA, genuine distinct authenticated code/architecture/security reviews with all major threads resolved, controlled physical NTFS sudden-power-loss evidence for Issue #73, and persisted deterministic risk-3 `ACCEPTED`. Neither diagnostic PR #119/#120 nor historical PR #70 is authorized to merge; no release or approval is claimed.\n\n- **T04-P2 v0.13 Control Center API projection compatibility (30 October 2026):** exact-source independent review of PR #92 found that `/v1/workflow` reads canonical V2 while `/v1/scale-context` still reads stale v0.12 legacy `current.json`, reporting an active fresh workflow as inactive and a migrated workflow as frozen. Isolated draft diagnostic [PR #118](https://github.com/eybersjp/development-kit/pull/118) test-only `f9a1972ccfcc5e7b2d580612ef9a94b63627aa46` reproduced the mismatch in Ubuntu [CI #41080813268](https://github.com/eybersjp/development-kit/actions/runs/41080813268) (RED). A narrow read-only authority-aware API projection prefers a verified active V2 layout, refuses stale legacy fallback when V2 metadata is invalid, preserves v0.12 legacy behavior for non-V2 projects, never creates a legacy pointer, and reports V2 as installed but **not independently accepted**. Explicit v0.13 contract scope expanded to `runtime/api/runtime-api-service.mjs`. Diagnostic SHA `85b8628fcc8ecc0b47eb7fa4d1a22c19e3996932` passed **both complete Ubuntu and Windows v0.13 validation jobs** in [CI #41081063299](https://github.com/eybersjp/development-kit/actions/runs/41081063299). Ported only validated API source, regression and scope to actual PR #92 in `4fe2fb7ab28a3c05d5b248e9dbdaf68b7b7567a2`, `00d5628beee97a5d1131ba1f726517530c51d2a5`, `e0b381d3861a81db7d8c866e83a28e8cd184baaa`. PR #118 remains draft, unmerged; the new exact integration head requires fresh CI and independent source-bound re-review. No physical Windows NTFS power-loss certification, risk-3 ACCEPTED verdict, mainline merge or release is claimed. +- **T04-P2 literal JSON patch-key compatibility (30 October 2026):** PR #92 exact-source review on `952ae0dec1ae7924286c37d3567ec571689efea7` identified that internally generated patch paths rejected valid JSON keys including empty strings, LF, CR and NUL. Isolated **draft, do-not-merge** diagnostic [PR #119](https://github.com/eybersjp/development-kit/pull/119) reproduced the defect with adversarial update/deletion tests on **both Ubuntu and Windows** in [CI #41088497647](https://github.com/eybersjp/development-kit/actions/runs/41088497647) (RED). The narrow correction permits any literal string segment in JSON array paths while retaining type validation and own-property-safe patch writes. Diagnostic `51f80f691d485e47dd201a42a3ae64545d1771fb` passed **both full Ubuntu/Windows v0.13 validation jobs** in [CI #41088653146](https://github.com/eybersjp/development-kit/actions/runs/41088653146) (GREEN). Only runtime and test were ported to actual v0.13 PR #92 in `f35455dff057cd550eccc4d0100f54292b65d711` and `b6a25cbc510dd8011fc43319dc7927f71b2b42c2`; diagnostic workflow exception was **not** ported. This does not establish independent acceptance or NTFS physical power-loss durability. + +- **T04-P2 non-mutating legacy Control Center projection (30 October 2026):** PR #92 exact-source review identified that a V2 metadata-only ledger plus unmigrated legacy workflow could cause read-only `GET /v1/scale-context` to repair a corrupt `current.json` pointer. Isolated **draft, do-not-merge** diagnostic [PR #120](https://github.com/eybersjp/development-kit/pull/120) reproduced unauthorized pointer repair and tested a valid unmigrated legacy pointer; **Ubuntu and Windows** [CI #41088803827](https://github.com/eybersjp/development-kit/actions/runs/41088803827) RED. Narrow correction distinguishes active V2 workflow authority from V2 metadata presence; unmigrated legacy workflows use the existing non-mutating pointer projection, and unreadable V2 migration evidence fails closed rather than authorizing fallback. Diagnostic `b9374618c054c1fd9b596b68b43ac402d954bf01` passed **both full Ubuntu/Windows v0.13 validation jobs** in [CI #41089162418](https://github.com/eybersjp/development-kit/actions/runs/41089162418) (GREEN). Only runtime/test were ported to actual v0.13 PR #92 in `fc1d6715d95cd530ed17fc2b3f1ae801269b0854` and `a28b3cce36c57323a9cabc8d291ed1cf1a26a951`. This does not establish independent acceptance or physical Windows power-loss assurance. + +- **Acceptance boundary after both fixes:** actual PR #92 must complete fresh full Ubuntu/Windows CI on the **combined final head**, two distinct complete runs on the same immutable reviewed SHA, genuine distinct authenticated code/architecture/security reviews with all major threads resolved, controlled physical NTFS sudden-power-loss evidence for Issue #73, and persisted deterministic risk-3 `ACCEPTED`. Neither diagnostic PR #119/#120 nor historical PR #70 is authorized to merge; no release or approval is claimed. + +- **T04-P2 v0.13 Control Center API projection compatibility (30 October 2026):** exact-source independent review of PR #92 found that `/v1/workflow` reads canonical V2 while `/v1/scale-context` still reads stale v0.12 legacy `current.json`, reporting an active fresh workflow as inactive and a migrated workflow as frozen. Isolated draft diagnostic [PR #118](https://github.com/eybersjp/development-kit/pull/118) test-only `f9a1972ccfcc5e7b2d580612ef9a94b63627aa46` reproduced the mismatch in Ubuntu [CI #41080813268](https://github.com/eybersjp/development-kit/actions/runs/41080813268) (RED). A narrow read-only authority-aware API projection prefers a verified active V2 layout, refuses stale legacy fallback when V2 metadata is invalid, preserves v0.12 legacy behavior for non-V2 projects, never creates a legacy pointer, and reports V2 as installed but **not independently accepted**. Explicit v0.13 contract scope expanded to `runtime/api/runtime-api-service.mjs`. Diagnostic SHA `85b8628fcc8ecc0b47eb7fa4d1a22c19e3996932` passed **both complete Ubuntu and Windows v0.13 validation jobs** in [CI #41081063299](https://github.com/eybersjp/development-kit/actions/runs/41081063299). Ported only validated API source, regression and scope to actual PR #92 in `4fe2fb7ab28a3c05d5b248e9dbdaf68b7b7567a2`, `00d5628beee97a5d1131ba1f726517530c51d2a5`, `e0b381d3861a81db7d8c866e83a28e8cd184baaa`. PR #118 remains draft, unmerged; the new exact integration head requires fresh CI and independent source-bound re-review. No physical Windows NTFS power-loss certification, risk-3 ACCEPTED verdict, mainline merge or release is claimed. - **W73-28 staged restore path alias hardening (30 October 2026):** draft, unmerged diagnostic [PR #116](https://github.com/eybersjp/development-kit/pull/116) introduced a signed-backup regression using distinct `s.json` and Unicode U+017F long-s paths. Existing lowercase-only normalized destination preflight silently accepted the pair on Ubuntu, reproduced as `Missing expected exception` in test-only [CI #41079128579](https://github.com/eybersjp/development-kit/actions/runs/41079128579) and [#41079372395](https://github.com/eybersjp/development-kit/actions/runs/41079372395) (RED). The narrow correction conservatively checks uppercase-folded destinations before staging and checks staged file dev/inode identities before replacing an existing destination, addressing additional NTFS 8.3/volume-specific aliases where textual folding is insufficient. Diagnostic SHA `d849880ece9a58f7bf9f56feab02882c028811c9` passed both full Ubuntu and Windows v0.13 validation lanes in [CI #41079431166](https://github.com/eybersjp/development-kit/actions/runs/41079431166) (GREEN). Only test and runtime files were ported to actual integration PR #92 in `5920cd352dae245d6360a59fd62672c1259bccb9` and `ea32b2bbff336f6dde4f581c2315b58f4a7e86b0`. The real NTFS 8.3 alias variant and sudden physical power-loss durability still require independent evidence; this test is a conservative cross-platform rejection control, **not** physical NTFS certification or T04 ACCEPTED. PR #116 must not merge. From 8de2b9cbefe4018f66b831853bfa54586b670c88 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 21:46:22 +0200 Subject: [PATCH 084/177] feat(t04): port verified risk-3 acceptance evidence 1/4 --- scripts/v013-t04-acceptance-gate.mjs | 291 +++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 scripts/v013-t04-acceptance-gate.mjs diff --git a/scripts/v013-t04-acceptance-gate.mjs b/scripts/v013-t04-acceptance-gate.mjs new file mode 100644 index 000000000..39e6085d3 --- /dev/null +++ b/scripts/v013-t04-acceptance-gate.mjs @@ -0,0 +1,291 @@ +#!/usr/bin/env node +// DKF130-T04 risk-3 acceptance: authenticated external evidence, never CI-green inference. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { loadDevelopmentContract, checkContractStaleness } from '../runtime/orchestration/development-contract.mjs'; +import { decideAcceptance, validateAcceptanceRecord } from '../runtime/orchestration/acceptance-engine.mjs'; +import { createVerificationRecord, evaluateControlCoverage, getRunDirectory, + persistVerificationRecord, persistControlManifest } from '../runtime/orchestration/evidence-store.mjs'; +import { createReviewResult } from '../runtime/orchestration/review-result.mjs'; +import { detectArchitectureDrift } from '../runtime/orchestration/architecture-drift.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const REPO = 'eybersjp/development-kit'; +const RECEIPTS = 'docs/04-architecture/dkf-v013-t04-independent-review-receipts.json'; +const ROLES = ['code-reviewer', 'architecture-reviewer', 'security-reviewer']; +const SHA = /^[a-f0-9]{40}$/; +const runId = process.env.GITHUB_RUN_ID; +const token = process.env.GITHUB_TOKEN; +assert.equal(process.env.GITHUB_ACTIONS, 'true', 'T04 verdict must originate in GitHub Actions'); +assert.equal(process.env.GITHUB_REPOSITORY, REPO); +assert.equal(process.env.GITHUB_EVENT_NAME, 'pull_request'); +assert.equal(process.env.GITHUB_JOB, 't04_acceptance'); +assert.match(runId || '', /^[1-9][0-9]*$/); +assert.ok(token, 'Authenticated GitHub API token required'); +const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')); +assert.equal(event.pull_request?.number, 92, 'Only v0.13 integration PR #92 can be accepted'); +assert.equal(event.pull_request?.head?.ref, 'feature/v0.13-state-engine-v2'); +assert.equal(event.pull_request?.head?.repo?.full_name, REPO); +const receipts = JSON.parse(fs.readFileSync(path.join(ROOT, RECEIPTS), 'utf8')); +assert.equal(receipts.schemaVersion, '1.0.0'); +assert.match(receipts.reviewedCommit || '', SHA, 'Missing exact-source independent reviews'); +assert.equal(receipts.status, 'INDEPENDENT_REVIEWS_COMPLETE', 'Review register must be complete'); +assert.ok(Array.isArray(receipts.reviews) && receipts.reviews.length === 3, + 'Three separate authenticated code, architecture and security reviews are mandatory'); +const reviewedCommit = receipts.reviewedCommit; + +async function api(endpoint) { + const response = await fetch('https://api.github.com/repos/' + REPO + '/' + endpoint, { + headers: { Authorization: 'Bearer ' + token, Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': 'dkf-v013-t04-acceptance' }, + }); + assert.ok(response.ok, 'GitHub evidence unavailable: ' + endpoint + ' (' + response.status + ')'); + return response.json(); +} +async function graphql(query, variables) { + const response = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json', + 'User-Agent': 'dkf-v013-t04-acceptance' }, + body: JSON.stringify({ query, variables }), + }); + assert.ok(response.ok, 'Authenticated review-thread query failed'); + const data = await response.json(); + assert.ok(!data.errors?.length, 'Review-thread API returned GraphQL errors'); + return data.data; +} +// A receipt-only follow-up commit is permitted; every other tracked blob and +// executable mode must match the immutable source actually reviewed. +const tree = await api('git/trees/' + reviewedCommit + '?recursive=1'); +assert.ok(!tree.truncated && Array.isArray(tree.tree), 'Complete reviewed tree required'); +const expected = new Map(tree.tree.filter(x => x.type === 'blob') + .map(x => [x.path, { sha: x.sha, mode: x.mode }])); +const index = execFileSync('git', ['ls-files', '--stage', '-z'], { cwd: ROOT, encoding: 'utf8' }); +const observed = new Map(); +for (const line of index.split('\0').filter(Boolean)) { + const m = /^(\d+) ([a-f0-9]{40}) \d+\t([\s\S]+)$/.exec(line); + assert.ok(m, 'Unparseable tracked Git index entry'); + observed.set(m[3], { sha: m[2], mode: m[1] }); +} +for (const [name, blob] of expected) { + if (name !== RECEIPTS) assert.deepEqual(observed.get(name), blob, + 'Unreviewed source change or deletion: ' + name); +} +for (const name of observed.keys()) { + if (name !== RECEIPTS) assert.ok(expected.has(name), 'Unreviewed new source: ' + name); +} +assert.equal(execFileSync('git', ['status', '--porcelain', '--untracked-files=no'], { + cwd: ROOT, encoding: 'utf8' }).trim(), '', 'Modified tracked source invalidates review'); +const pr = await api('pulls/92'); +assert.equal(pr.state, 'open'); +assert.equal(pr.head.sha, event.pull_request.head.sha, 'Stale PR event/source'); +assert.equal(pr.base.ref, 'main'); +assert.equal(pr.head.repo.full_name, REPO); + +// Distinct GitHub-authored, post-source reviews. Self-authored PASS strings are +// never an approval. Each review must explicitly identify its role and SHA. +const reviewers = new Set(); +const authenticatedReviews = new Map(); +const reviewIds = new Set(); +for (const role of ROLES) { + const receipt = receipts.reviews.find(x => x?.role === role); + assert.ok(receipt, 'Missing independent role: ' + role); + assert.ok(Number.isSafeInteger(receipt.githubReviewId) && receipt.githubReviewId > 0); + assert.ok(!reviewIds.has(receipt.githubReviewId), 'Review reused for multiple roles'); + reviewIds.add(receipt.githubReviewId); + const review = await api('pulls/92/reviews/' + receipt.githubReviewId); + assert.equal(review.commit_id, reviewedCommit, 'Review is stale'); + assert.equal(review.state, 'APPROVED', 'Only authenticated GitHub approval is PASS'); + assert.ok(review.user?.login && review.user.login !== pr.user.login, + 'Author cannot independently approve their own code'); + assert.ok(!reviewers.has(review.user.login), 'Distinct reviewer identity required for each role'); + reviewers.add(review.user.login); + authenticatedReviews.set(role, review); + assert.ok((review.body || '').includes('DKF130-T04 ' + role + ' ' + reviewedCommit), + 'Approval must explicitly bind role and exact source SHA'); + const comments = await api('pulls/92/reviews/' + receipt.githubReviewId + '/comments?per_page=1'); + assert.ok(Array.isArray(comments) && comments.length === 0, + 'Review with findings is not a clean acceptance review'); +} +assert.equal(reviewIds.size, 3); +const query = `query($cursor:String){repository(owner:"eybersjp",name:"development-kit"){pullRequest(number:92){reviewThreads(first:100,after:$cursor){pageInfo{hasNextPage endCursor} nodes{isResolved}}}}}`; +let cursor = null; +for (;;) { + const data = await graphql(query, { cursor }); + const threads = data?.repository?.pullRequest?.reviewThreads; + assert.ok(Array.isArray(threads?.nodes), 'Review threads unavailable'); + assert.ok(threads.nodes.every(t => t.isResolved === true), 'Unresolved P1/P2 or other review threads'); + if (!threads.pageInfo?.hasNextPage) break; + assert.ok(threads.pageInfo.endCursor && threads.pageInfo.endCursor !== cursor); + cursor = threads.pageInfo.endCursor; +} +// A closed issue is necessary, not sufficient: retain independent physical +// power-loss evidence and externally verified artifact provenance. +const durabilityIssue = await api('issues/73'); +assert.equal(durabilityIssue.state, 'closed', 'Physical NTFS durability blocker #73 remains open'); +assert.notEqual(durabilityIssue.state_reason, 'not_planned', + 'Issue #73 cannot be waived instead of resolved'); +const acceptanceTrackingIssue = await api('issues/71'); +assert.notEqual(acceptanceTrackingIssue.state_reason, 'not_planned', + 'Risk-3 acceptance blocker #71 cannot be waived'); +// Issue #71 is the acceptance tracking ticket and may be closed AFTER the +// persisted ACCEPTED verdict; requiring closure here would deadlock the gate. +const physical = receipts.windowsPhysicalPowerLoss; +assert.ok(physical && physical.reviewedCommit === reviewedCommit + && typeof physical.artifactUrl === 'string' + && physical.artifactUrl.startsWith('https://github.com/' + REPO + '/actions/runs/') + && typeof physical.filesystem === 'string' && /^NTFS\b/.test(physical.filesystem) + && typeof physical.artifactName === 'string' && physical.artifactName.length > 0 + && /^sha256:[a-f0-9]{64}$/.test(physical.artifactDigest || '') + && Number.isSafeInteger(physical.independentReviewId) + && physical.cleanupSafety === 'CLEANUP_SAFE' + && physical.originalTreesPreserved === true + && physical.hashChainedJournalRecovered === true, + 'Independent, source-bound physical NTFS power-loss evidence is mandatory'); +const physicalReview = await api('pulls/92/reviews/' + physical.independentReviewId); +assert.equal(physicalReview.state, 'APPROVED'); +assert.ok(physicalReview.user?.login && physicalReview.user.login !== pr.user.login, + 'Physical NTFS power-loss reviewer must be independent of the source author'); +assert.equal(physicalReview.commit_id, reviewedCommit); +assert.ok((physicalReview.body || '').includes('DKF130-T04 NTFS-PHYSICAL-POWER-LOSS ' + reviewedCommit), + 'Independent physical test review must explicitly bind source and evidence'); +const artifactRun = Number(physical.artifactUrl.match(/\/actions\/runs\/([0-9]+)/)?.[1]); +assert.ok(Number.isSafeInteger(artifactRun)); +const artifact = await api('actions/runs/' + artifactRun); +assert.equal(artifact.head_sha, reviewedCommit); +assert.equal(artifact.conclusion, 'success'); +const artifacts = await api('actions/runs/' + artifactRun + '/artifacts?per_page=100'); +const physicalArtifact = artifacts.artifacts?.find(x => x.name === physical.artifactName); +assert.ok(physicalArtifact && !physicalArtifact.expired && + physicalArtifact.digest === physical.artifactDigest, + 'Authenticated immutable physical NTFS power-loss artifact and SHA-256 digest required'); +assert.ok((physicalReview.body || '').includes(physical.artifactDigest), + 'Independent physical review must bind the exact artifact digest'); + +// Two distinct full Ubuntu/Windows workflows on the exact reviewed commit. +// The current run does not count while its acceptance job is executing. +assert.ok(Array.isArray(receipts.validationRunIds) && receipts.validationRunIds.length >= 2); +const validationRuns = new Set(receipts.validationRunIds); +assert.equal(validationRuns.size, receipts.validationRunIds.length); +for (const id of validationRuns) { + assert.ok(Number.isSafeInteger(id) && id > 0 && String(id) !== runId); + const run = await api('actions/runs/' + id); + assert.equal(run.head_sha, reviewedCommit, 'Validation run is not exact-source'); + assert.equal(run.event, 'pull_request'); + assert.equal(run.status, 'completed'); + // Diagnostic/production acceptance jobs may fail while validation succeeds: + // verify both full platform jobs individually, not overall run conclusion. + const jobs = []; + for (let page = 1; ; page++) { + const batch = await api('actions/runs/' + id + '/jobs?per_page=100&page=' + page); + jobs.push(...batch.jobs); + if (batch.jobs.length < 100) break; + } + for (const os of ['ubuntu-latest', 'windows-latest']) { + const matches = jobs.filter(job => job.name === 'validate (' + os + ')'); + assert.equal(matches.length, 1, 'Missing unique full validation for ' + os); + assert.equal(matches[0].conclusion, 'success', 'Full validation failed on ' + os); + } +} +// The canonical Development Contract acceptance engine, not a bespoke green +// flag, is the sole authority to produce a persisted ACCEPTED verdict. +const contract = loadDevelopmentContract('INC-DKF130-T04', ROOT); +assert.ok(contract, 'Persisted v0.13 Development Contract missing'); +assert.equal(contract.taskId, 'DKF130-T04'); +assert.equal(contract.risk.level, 3); +assert.equal(checkContractStaleness(contract, ROOT).stale, false); +const evidenceRunId = 'T04-V013-' + reviewedCommit.slice(0, 16); +const verifiedCiEvidence = [...validationRuns].map(id => ({ + type: 'test', deterministicVerification: true, verificationType: 'test', + url: 'https://github.com/' + REPO + '/actions/runs/' + id, + evidenceId: 'GITHUB-ACTIONS-' + id, reviewedCommit, +})); +const criteria = contract.acceptanceCriteria.map(c => ({ + id: c.id, status: 'PASS', + evidence: c.id === 'DKF-120-AC-015' + ? [...verifiedCiEvidence, { + type: 'external', authoritativeExternalState: true, + verificationType: 'security', + url: physical.artifactUrl, reviewedCommit, + evidenceId: 'INDEPENDENT-NTFS-POWER-LOSS-' + physical.independentReviewId, + }] + : verifiedCiEvidence, +})); +const verification = createVerificationRecord({ + contract, runId: evidenceRunId, role: 'test-engineer', + sourceFingerprint: contract.sourceFingerprint, + contextIsolation: 'rehydrated', criteria, +}); +const reviews = ROLES.map(role => createReviewResult({ + contract, runId: evidenceRunId, role, + sourceFingerprint: contract.sourceFingerprint, + contextIsolation: 'rehydrated', + createdAt: authenticatedReviews.get(role).submitted_at, + findings: [], +})); +const requiredControls = [ + { id: 'ledger-integrity', statement: 'Canonical event chain and index recovery remain integrity protected' }, + { id: 'multi-process-concurrency', statement: 'Concurrent writes and bootstrap recovery preserve committed events' }, + { id: 'acl-path-isolation', statement: 'Restore and canonical paths reject link and alias escapes' }, + { id: 'ntfs-crash-restore', statement: 'Supported NTFS sudden-power-loss restore and journal recovery verified' }, + { id: 'source-provenance', statement: 'Review and CI are bound to immutable final source' }, +]; +assert.ok(Array.isArray(receipts.securityControls) && + receipts.securityControls.length === requiredControls.length, + 'Authenticated independent security-control evidence is mandatory'); +const controlResults = []; +for (const control of requiredControls) { + const witness = receipts.securityControls.find(x => x?.id === control.id); + assert.ok(witness && witness.reviewedCommit === reviewedCommit && + Number.isSafeInteger(witness.githubReviewId) && + Number.isSafeInteger(witness.workflowRunId), + 'Security control missing independent source-bound witness: ' + control.id); + assert.equal(witness.githubReviewId, authenticatedReviews.get('security-reviewer').id, + 'Security control witnesses must bind the independently approved security review'); + const witnessReview = await api('pulls/92/reviews/' + witness.githubReviewId); + assert.equal(witnessReview.state, 'APPROVED'); + assert.equal(witnessReview.commit_id, reviewedCommit); + assert.ok((witnessReview.body || '').includes('DKF130-T04 CONTROL ' + control.id + ' ' + reviewedCommit)); + const witnessRun = await api('actions/runs/' + witness.workflowRunId); + assert.equal(witnessRun.head_sha, reviewedCommit); + assert.equal(witnessRun.conclusion, 'success'); + controlResults.push({ + id: control.id, status: 'PASS', + evidence: [{ type: 'test', deterministicVerification: true, + reviewedCommit, evidenceId: 'CONTROL-' + control.id + '-' + witness.workflowRunId, + url: 'https://github.com/' + REPO + '/actions/runs/' + witness.workflowRunId }], + }); +} +const security = evaluateControlCoverage({ + contractId: contract.contractId, runId: evidenceRunId, + domain: 'security', expectedControls: requiredControls, results: controlResults, +}); +const architectureDrift = detectArchitectureDrift({ + baseline: { storageTechnologies: ['event-jsonl', 'derived-snapshot', 'disposable-json-index'], + migrationStrategies: ['legacy-migration'] }, + current: { storageTechnologies: ['event-jsonl', 'derived-snapshot', 'disposable-json-index'], + migrationStrategies: ['legacy-migration', 'explicit-offline-backup-recovery'] }, + expectedChanges: ['migration-strategy:explicit-offline-backup-recovery'], +}); +const decision = decideAcceptance({ + contract, verification, reviews, controlManifests: [security], + architectureDrift, rootDir: ROOT, +}); +validateAcceptanceRecord(decision); +assert.equal(decision.state, 'ACCEPTED', + 'Deterministic risk-3 gate requires complete canonical verification, control and review records; never infer ACCEPTED from CI'); +const runDirectory = getRunDirectory(ROOT, contract.contractId, evidenceRunId); +fs.mkdirSync(runDirectory, { recursive: true }); +persistVerificationRecord(verification, ROOT); +persistControlManifest(security, ROOT); +const destination = path.join(ROOT, '.development-kit', 'acceptance', 'DKF130-T04.json'); +fs.mkdirSync(path.dirname(destination), { recursive: true }); +fs.writeFileSync(destination, JSON.stringify({ + ...decision, reviewedCommit, validationRunIds: [...validationRuns], + independentReviewIds: [...reviewIds], physicalPowerLoss: physical, + verification, reviews, securityControl: security, architectureDrift, +}, null, 2) + '\n', { flag: 'wx' }); +console.log('DKF130-T04 ACCEPTED ' + reviewedCommit); From 621dc389030732cdc6101a38cafa3b147195effc Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 21:46:26 +0200 Subject: [PATCH 085/177] feat(t04): port verified risk-3 acceptance evidence 2/4 --- .../v013-t04-acceptance-ci-contract.test.mjs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 scripts/v013-t04-acceptance-ci-contract.test.mjs diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs new file mode 100644 index 000000000..090dfb2dc --- /dev/null +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -0,0 +1,67 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const workflow = fs.readFileSync(path.join(root, '.github/workflows/ci.yml'), 'utf8'); + +test('T04-A13 v0.13 integration has a distinct source-bound risk-3 acceptance job', () => { + assert.match(workflow, /^ t04_acceptance:\s*$/m, + 'v0.13 T04 must not rely on green platform tests as its acceptance verdict'); + const job = workflow.split(/^ t04_acceptance:\s*$/m)[1]?.split(/^ [a-z][\w-]*:\s*$/m)[0] ?? ''; + assert.match(job, /^ needs:\s*validate\s*$/m, + 'independent acceptance must depend on successful Ubuntu AND Windows matrix validation'); + assert.match(job, /github\.head_ref\s*==\s*'feature\/v0\.13-state-engine-v2'/, + 'historical/diagnostic PRs must never receive production T04 acceptance'); + assert.match(job, /scripts\/v013-t04-acceptance-gate\.mjs/, + 'the acceptance job must run a deterministic risk-3 gate, not a placeholder echo'); + assert.match(job, /GITHUB_TOKEN:/, + 'independent reviews and CI provenance must be checked with authenticated GitHub evidence'); +}); + +test('T04-A14 acceptance gate source exists and requires independent immutable evidence', () => { + const gatePath = path.join(root, 'scripts/v013-t04-acceptance-gate.mjs'); + assert.ok(fs.existsSync(gatePath), 'missing deterministic v0.13 T04 acceptance gate'); + const source = fs.readFileSync(gatePath, 'utf8'); + for (const required of ['decideAcceptance', 'reviewedCommit', 'architecture-reviewer', + 'code-reviewer', 'security-reviewer', 'GITHUB_RUN_ID', 'GITHUB_TOKEN', + 'CLEANUP_SAFE', 'windows']) { + assert.ok(source.includes(required), 'acceptance gate lacks required evidence check: ' + required); + } +}); + +test('T04-A15 risk-3 gate parses, fails closed outside authenticated CI and never uses historical PR #70', () => { + const gate = path.join(root, 'scripts/v013-t04-acceptance-gate.mjs'); + const syntax = spawnSync(process.execPath, ['--check', gate], { cwd: root, encoding: 'utf8' }); + assert.equal(syntax.status, 0, syntax.stderr); + const isolated = spawnSync(process.execPath, [gate], { + cwd: root, encoding: 'utf8', env: { + ...process.env, GITHUB_ACTIONS: 'false', GITHUB_TOKEN: '', + GITHUB_REPOSITORY: 'eybersjp/development-kit', + }, + }); + assert.notEqual(isolated.status, 0, 'Unauthenticated local invocation must never produce ACCEPTED'); + assert.doesNotMatch(isolated.stdout, /DKF130-T04 ACCEPTED/); + const code = fs.readFileSync(gate, 'utf8'); + assert.match(code, /decideAcceptance\(\{/); + assert.match(code, /assert\.equal\(decision\.state, 'ACCEPTED'/); + assert.match(code, /physical\.artifactUrl/); + assert.match(code, /validationRunIds/); + assert.match(code, /review\.commit_id, reviewedCommit/); + assert.match(code, /review\.state, 'APPROVED'/); + assert.match(code, /securityControls/); +}); + +test('T04-A16 acceptance workflow reads exact PR head with least privilege', () => { + const job = workflow.split(/^ t04_acceptance:\s*$/m)[1]?.split(/^ [a-z][\w-]*:\s*$/m)[0] ?? ''; + assert.match(job, /ref: \$\{\{ github\.event\.pull_request\.head\.sha \}\}/); + assert.match(job, /persist-credentials: false/); + assert.match(job, /contents: read/); + assert.match(job, /pull-requests: read/); + assert.match(job, /issues: read/); + assert.match(job, /actions: read/); + assert.doesNotMatch(job, /contents: write|pull-requests: write|issues: write|actions: write/); +}); From 7c2b6b039c656ce66a8b39aa839afd16d4537255 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 21:46:29 +0200 Subject: [PATCH 086/177] feat(t04): port verified risk-3 acceptance evidence 3/4 --- .../dkf-v013-t04-independent-review-receipts.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/04-architecture/dkf-v013-t04-independent-review-receipts.json diff --git a/docs/04-architecture/dkf-v013-t04-independent-review-receipts.json b/docs/04-architecture/dkf-v013-t04-independent-review-receipts.json new file mode 100644 index 000000000..ec293e400 --- /dev/null +++ b/docs/04-architecture/dkf-v013-t04-independent-review-receipts.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": "1.0.0", + "reviewedCommit": null, + "reviews": [], + "validationRunIds": [], + "windowsPhysicalPowerLoss": null, + "status": "PENDING_INDEPENDENT_REVIEWS", + "policy": "Risk-3 requires three distinct authenticated exact-source GitHub approvals, two full cross-platform runs, physical NTFS power-loss evidence and a canonical persisted Development Contract ACCEPTED verdict." +} From c2635e64470e131ae5411074b7280ea92e19155a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 21:46:32 +0200 Subject: [PATCH 087/177] feat(t04): port verified risk-3 acceptance evidence 4/4 --- .../scripts/v013-t04-acceptance-gate.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/03-reference/scripts/v013-t04-acceptance-gate.md diff --git a/docs/03-reference/scripts/v013-t04-acceptance-gate.md b/docs/03-reference/scripts/v013-t04-acceptance-gate.md new file mode 100644 index 000000000..51144a544 --- /dev/null +++ b/docs/03-reference/scripts/v013-t04-acceptance-gate.md @@ -0,0 +1,29 @@ +# v013-t04-acceptance-gate.mjs + +## Purpose + +`scripts/v013-t04-acceptance-gate.mjs` is the fail-closed, risk-3 DKF130-T04 acceptance evaluator for the **v0.13.0 integration candidate PR #92**. It is not a general release approval and must never run as a shortcut around independent reviews or Windows restore durability evidence. + +## Execution context + +The script requires an authenticated GitHub Actions pull-request event for `eybersjp/development-kit`, PR #92, from branch `feature/v0.13-state-engine-v2`. The dedicated `t04_acceptance` job must depend on both Ubuntu and Windows validation jobs. The job cannot authenticate evidence from a local invocation or an unrelated diagnostic PR. + +## Required independent evidence + +The committed `docs/04-architecture/dkf-v013-t04-independent-review-receipts.json` register must bind one immutable reviewed source commit and three distinct, authenticated GitHub `APPROVED` reviews for code, architecture, and security, with exact SHA and role scope. All review threads must be resolved. Review metadata is fetched from GitHub, not trusted from a self-authored receipt. + +Issue #73 must be resolved without waiver. Controlled NTFS physical power-loss evidence must be source-bound, independently reviewed and supported by a verified GitHub Actions artifact and digest. Two distinct full Ubuntu/Windows workflow executions must have passed on the identical reviewed commit. The source tree, Git index and executable modes must match the reviewed commit except for the receipt-only follow-up. + +## Deterministic decision + +After authenticated evidence verification, the script constructs the v0.13 Development Contract verification record, independent review records, security control coverage, and architecture drift assessment. The canonical acceptance engine must return `ACCEPTED` before any acceptance record is written. + +The acceptance record is written under `.development-kit/acceptance/DKF130-T04.json` in the runner workspace. **A runner-local file is not, by itself, a durable repository-persisted acceptance verdict.** Release integration requires independently verified retention and retrieval of the authentic record through an approved governance artifact or repository persistence mechanism; this diagnostic implementation does not authorize a merge or release merely because its job passes. + +## Usage and limits + +```bash +node scripts/v013-t04-acceptance-gate.mjs +``` + +The command is intended to run only inside its restricted GitHub Actions job. It does not publish releases, close blocking issues, resolve review threads, modify production state or waive full Windows legacy-backup restoration. Source-bound reviews and physical sudden-power-loss evidence cannot be simulated by ordinary CI. From 2490b27248d612904415240be8dbc656eee1da51 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 21:51:32 +0200 Subject: [PATCH 088/177] docs(v0.13): index risk-3 T04 acceptance reference (CI red-to-green) --- docs/SUMMARY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 3ad8e45f1..7d6b7db91 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -196,6 +196,7 @@ * [phase04-workspace-target-engine-contract](03-reference/scripts/phase04-workspace-target-engine-contract.md) * [phase05-execution-capsule-contract](03-reference/scripts/phase05-execution-capsule-contract.md) * [v013-state-engine-v2-contract](03-reference/scripts/v013-state-engine-v2-contract.md) +* [v013-t04-acceptance-gate](03-reference/scripts/v013-t04-acceptance-gate.md) * [state-engine-v2-benchmark](03-reference/scripts/state-engine-v2-benchmark.md) * [t04-review-thread-blockers](03-reference/scripts/t04-review-thread-blockers.md) * [sync-plugin](03-reference/scripts/sync-plugin.md) From 6ffa5a6e2dd4c7f1f7f162921ab1481611b04093 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 21:54:08 +0200 Subject: [PATCH 089/177] ci(v0.13): enforce independently reviewed source-bound T04 risk-3 acceptance after both OS lanes --- .github/workflows/ci.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c72b44f82..6c6c6b2c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,3 +135,41 @@ jobs: - name: Validate v0.13 full compatibility regression if: ${{ hashFiles('runtime/orchestration/state-engine-v2.mjs') != '' }} run: npm run v013:regression + + - name: Verify v0.13 risk-3 acceptance job contract (negative control) + run: node --test scripts/v013-t04-acceptance-ci-contract.test.mjs + + t04_acceptance: + name: T04 independently reviewed risk-3 acceptance + if: ${{ github.event_name == 'pull_request' && github.head_ref == 'feature/v0.13-state-engine-v2' }} + needs: validate + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pull-requests: read + issues: read + actions: read + steps: + - name: Check out exact reviewed integration source + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 22 + - name: Establish exact-source v0.13 Development Contract + run: node scripts/v013-state-engine-v2-contract.mjs + - name: Verify immutable external T04 acceptance evidence + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node scripts/v013-t04-acceptance-gate.mjs + - name: Preserve canonical T04 verdict evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: dkf-v013-t04-acceptance-evidence + path: .development-kit/acceptance/DKF130-T04.json + if-no-files-found: ignore From a40b3186f38a52c3d66cda2e6b8b26cdd508417f Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 21:54:57 +0200 Subject: [PATCH 090/177] test(v0.13): scope branch-agnostic CI invariant to validation job, not independent acceptance --- scripts/v071-regression.test.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/v071-regression.test.mjs b/scripts/v071-regression.test.mjs index 717ae22ea..7640e947b 100644 --- a/scripts/v071-regression.test.mjs +++ b/scripts/v071-regression.test.mjs @@ -396,7 +396,12 @@ test('V13-CI-ROUTING: v0.13 validation is selected by source presence, not mutab const sourceProbe = String.raw`hashFiles\('runtime/orchestration/state-engine-v2\.mjs'\)`; const positive = new RegExp(String.raw`if:\s*\$\{\{\s*${sourceProbe}\s*!=\s*''\s*\}\}`); const negative = new RegExp(String.raw`if:\s*\$\{\{\s*${sourceProbe}\s*==\s*''\s*\}\}`); - assert.doesNotMatch(workflow, /github\.head_ref/, 'Branch-name conditions can silently skip T04 validation after a merge or branch rename'); + // Source-dependent validation must never be branch-gated. The distinct + // risk-3 acceptance job is deliberately restricted to the integration PR. + const validateJob = workflow.split(/^ validate:\s*$/m)[1]?.split(/^ [a-z][\w-]*:\s*$/m)[0] ?? ''; + assert.ok(validateJob, 'Source-dependent validate job must exist'); + assert.doesNotMatch(validateJob, /github\.head_ref/, + 'Branch-name conditions can silently skip T04 validation after a merge or branch rename'); assert.match(workflow, positive, 'v0.13 test lane must run whenever its canonical runtime is in the checkout'); assert.match(workflow, negative, 'v0.12-only release lane must run only without the v0.13 runtime'); assert.match(workflow, /run: npm run v013:regression/, 'Source-bound v0.13 lane must include full compatibility regression'); From 66b5c4770570d5c427217ead5058ba6f14500d4e Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 21:57:44 +0200 Subject: [PATCH 091/177] docs(v0.13): record test-first acceptance-gate enforcement and remaining authentic risk-3 blockers --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index 478b2aa00..e307177b1 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **Risk-3 deterministic acceptance enforcement (30 October 2026, NOT ACCEPTED):** actual integration draft PR #92 now includes a distinct source-bound `t04_acceptance` GitHub Actions job depending on both complete Ubuntu/Windows validation jobs, with read-only authenticated review/CI provenance, pending receipts, physical NTFS evidence requirements, canonical `decideAcceptance` and uploaded verdict artifact only on genuine success. Diagnostic draft PR #121 first reproduced the absent gate in RED Ubuntu/Windows CI #41101076526; corrected diagnostic SHA `6e228fb02f29f07131f651fc4f7077ddc1f39ab6` passed both full platform lanes in CI #41116732941 (acceptance correctly skipped on diagnostic). The PR #92 documentation-index regression was reproduced in CI #41118003732 and fixed at `2490b27248d612904415240be8dbc656eee1da51`; append-only gate workflow ported at `6ffa5a6e2dd4c7f1f7f162921ab1481611b04093`. A historical regression overreached by rejecting the intentionally branch-restricted acceptance job; RED Ubuntu CI #41119009453 and previously validated narrow test correction ported at `a40b3186f38a52c3d66cda2e6b8b26cdd508417f`. Production-head CI #41119119763 was in progress at this checkpoint, **not** claimed green. The independent gate must fail closed while 13 PR #92 threads remain unresolved, three distinct exact-source code/architecture/security approvals are missing, Issue #73 lacks controlled physical NTFS sudden-power-loss evidence, and two distinct complete same-final-SHA runs and a genuinely persisted `ACCEPTED` are absent. PR #92 remains draft/unmerged; no T04 v0.12 merge or v0.13 release is authorized. Repository Issue #84 records source/CI provenance in comment #6532264975. + - **T04-P2 literal JSON patch-key compatibility (30 October 2026):** PR #92 exact-source review on `952ae0dec1ae7924286c37d3567ec571689efea7` identified that internally generated patch paths rejected valid JSON keys including empty strings, LF, CR and NUL. Isolated **draft, do-not-merge** diagnostic [PR #119](https://github.com/eybersjp/development-kit/pull/119) reproduced the defect with adversarial update/deletion tests on **both Ubuntu and Windows** in [CI #41088497647](https://github.com/eybersjp/development-kit/actions/runs/41088497647) (RED). The narrow correction permits any literal string segment in JSON array paths while retaining type validation and own-property-safe patch writes. Diagnostic `51f80f691d485e47dd201a42a3ae64545d1771fb` passed **both full Ubuntu/Windows v0.13 validation jobs** in [CI #41088653146](https://github.com/eybersjp/development-kit/actions/runs/41088653146) (GREEN). Only runtime and test were ported to actual v0.13 PR #92 in `f35455dff057cd550eccc4d0100f54292b65d711` and `b6a25cbc510dd8011fc43319dc7927f71b2b42c2`; diagnostic workflow exception was **not** ported. This does not establish independent acceptance or NTFS physical power-loss durability. - **T04-P2 non-mutating legacy Control Center projection (30 October 2026):** PR #92 exact-source review identified that a V2 metadata-only ledger plus unmigrated legacy workflow could cause read-only `GET /v1/scale-context` to repair a corrupt `current.json` pointer. Isolated **draft, do-not-merge** diagnostic [PR #120](https://github.com/eybersjp/development-kit/pull/120) reproduced unauthorized pointer repair and tested a valid unmigrated legacy pointer; **Ubuntu and Windows** [CI #41088803827](https://github.com/eybersjp/development-kit/actions/runs/41088803827) RED. Narrow correction distinguishes active V2 workflow authority from V2 metadata presence; unmigrated legacy workflows use the existing non-mutating pointer projection, and unreadable V2 migration evidence fails closed rather than authorizing fallback. Diagnostic `b9374618c054c1fd9b596b68b43ac402d954bf01` passed **both full Ubuntu/Windows v0.13 validation jobs** in [CI #41089162418](https://github.com/eybersjp/development-kit/actions/runs/41089162418) (GREEN). Only runtime/test were ported to actual v0.13 PR #92 in `fc1d6715d95cd530ed17fc2b3f1ae801269b0854` and `a28b3cce36c57323a9cabc8d291ed1cf1a26a951`. This does not establish independent acceptance or physical Windows power-loss assurance. From f030c3ecbc53c16f126af56f30fd1b34d486518d Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 21:58:51 +0200 Subject: [PATCH 092/177] fix(v0.13): bound Windows state-lock wait for verified NTFS multi-process contention --- runtime/orchestration/state-engine-v2.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 906f3b140..a0f09c5e5 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -369,7 +369,10 @@ function atomicWrite(filePath, content, pinnedDirFd = null) { } } -function acquireStateLock(rootDir, timeoutMs = 12000) { +// Windows NTFS directory flushes and Defender I/O can extend a valid +// four-process writer queue beyond 12s under concurrent full CI. Keep a +// bounded longer wait on Windows; never reclaim a live owner or bypass locks. +function acquireStateLock(rootDir, timeoutMs = process.platform === 'win32' ? 20_000 : 12_000) { const paths = statePaths(rootDir); fs.mkdirSync(paths.stateRoot, { recursive: true }); assertStateRootRealpathSafe(paths); From 77bfac8f398b733f16ebf4ea55f272e3c96d0069 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 22:56:08 +0200 Subject: [PATCH 093/177] test(t04): preserve pending evidence when snapshot witness I/O fails (RED both OS) --- scripts/state-engine-v2-hardening.test.mjs | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index 9c68619e5..7eb4bd971 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -1110,3 +1110,47 @@ test('T04-H52 corrupted pending suffix is rejected before mutating canonical byt 'Rejected pending journal must remain available for operator diagnosis'); } }); + + +test('T04-H56 pending recovery preserves canonical evidence when snapshot witness read is denied', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const baseline = fs.readFileSync(paths.events, 'utf8'); + appendMetadataEvent({ rootDir, eventType: 'FOLLOWUP_EVENT', payload: { value: 2 } }); + const suffix = fs.readFileSync(paths.events, 'utf8').slice(baseline.length); + const torn = baseline + suffix.slice(0, Math.max(1, Math.floor(suffix.length / 3))); + fs.writeFileSync(paths.events, torn, 'utf8'); + fs.writeFileSync(paths.pending, JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(baseline), + previousHash: 'sha256:' + createHash('sha256').update(baseline).digest('hex'), + suffix, + }), 'utf8'); + + const originalRead = fs.readFileSync; + let denied = 0; + fs.readFileSync = function denySnapshotWitnessRead(target, ...args) { + if (path.basename(String(target)) === path.basename(paths.snapshot)) { + denied += 1; + const error = new Error('Synthetic snapshot witness permission denial'); + error.code = 'EACCES'; + throw error; + } + return originalRead.call(this, target, ...args); + }; + try { + assert.throws( + () => loadStateSnapshot(rootDir, { rebuildIfNeeded: true }), + /snapshot witness|EACCES|permission denial/i, + 'A denied snapshot witness read must fail closed before canonical repair', + ); + } finally { + fs.readFileSync = originalRead; + } + assert.ok(denied > 0, 'Fault injection must intercept the witness read'); + assert.equal(fs.readFileSync(paths.events, 'utf8'), torn, + 'Failed witness read must not truncate or append canonical event bytes'); + assert.equal(fs.existsSync(paths.pending), true, + 'Failed witness read must retain the pending journal for operator recovery'); +}); From a2dc909cb69f5e73acfd1a038f9c8272a7e2c94a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 22:56:16 +0200 Subject: [PATCH 094/177] fix(t04): fail closed on snapshot witness read errors before canonical recovery --- runtime/orchestration/state-engine-v2.mjs | 26 +++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index a0f09c5e5..c5ba06b15 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -1497,11 +1497,29 @@ function recoverPendingCommitLocked(rootDir) { // Validate the entire proposed ledger and the materialized snapshot witness // before any truncate/append/fsync can mutate canonical crash evidence. let witness = null; + let witnessBytes; try { - witness = JSON.parse(fs.readFileSync(snapshotTarget, 'utf8')); - validateStateSnapshot(witness); - } catch { - witness = null; + witnessBytes = fs.readFileSync(snapshotTarget, 'utf8'); + } catch (error) { + // A missing disposable snapshot may be rebuilt from canonical events. + // Permission, sharing and I/O failures are different: ignoring them + // would allow canonical ledger mutation without inspecting an existing + // witness that may prove the proposed pending suffix is stale. + if (error.code !== 'ENOENT') { + throw new StateEngineError( + 'Cannot inspect materialized snapshot witness before pending recovery: ' + + (error.code || error.message), + ); + } + } + if (witnessBytes !== undefined) { + try { + witness = JSON.parse(witnessBytes); + validateStateSnapshot(witness); + } catch { + // Malformed derived state is disposable, unlike unreadable evidence. + witness = null; + } } if (witness && witness.lastEventSequence > canonical.lastEventSequence) { throw new StateEngineError('Cannot recover a pending commit over a later snapshot integrity witness'); From f3bc71988cc8a472aaeabefc2397ebe5db4c1d85 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 22:56:26 +0200 Subject: [PATCH 095/177] docs(t04): record H56 cross-platform red/green evidence and remaining gates --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index e307177b1..91e7af7c9 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-H56 pending-recovery snapshot-witness I/O fail-closed (30 October 2026; NOT ACCEPTED):** Actual integration draft PR #92 inherited a recovery path that treated `EACCES`/I/O failure reading the materialized snapshot witness as if it were a disposable invalid snapshot, permitting canonical ledger truncation/repair and pending-journal acknowledgement without inspecting a potentially newer witness. Isolated draft diagnostic [PR #122](https://github.com/eybersjp/development-kit/pull/122) used descriptor-relative-path-aware `T04-H56` fault injection and reproduced the missing fail-closed rejection on **Ubuntu and Windows** in [RED CI #41126196799](https://github.com/eybersjp/development-kit/actions/runs/41126196799). Narrow correction separates filesystem read errors from JSON/derived-schema corruption: only `ENOENT` permits absent-witness recovery; other read errors abort before any canonical mutation, preserving original ledger and pending journal. Diagnostic `ee0e5f69b9f619b87a694bb3f93d3790873323c2` passed both complete Ubuntu/Windows v0.13 compatibility lanes in [GREEN CI #41126337280](https://github.com/eybersjp/development-kit/actions/runs/41126337280). Only regression test `77bfac8f398b733f16ebf4ea55f272e3c96d0069` and runtime correction `a2dc909cb69f5e73acfd1a038f9c8272a7e2c94a` were ported to PR #92; diagnostic PR #122 must not merge. Production-head CI and exact-source independent reviews must be rerun; NTFS physical sudden-power-loss Issue #73, unresolved PR threads, and authentic persisted risk-3 ACCEPTED remain blockers. + - **Risk-3 deterministic acceptance enforcement (30 October 2026, NOT ACCEPTED):** actual integration draft PR #92 now includes a distinct source-bound `t04_acceptance` GitHub Actions job depending on both complete Ubuntu/Windows validation jobs, with read-only authenticated review/CI provenance, pending receipts, physical NTFS evidence requirements, canonical `decideAcceptance` and uploaded verdict artifact only on genuine success. Diagnostic draft PR #121 first reproduced the absent gate in RED Ubuntu/Windows CI #41101076526; corrected diagnostic SHA `6e228fb02f29f07131f651fc4f7077ddc1f39ab6` passed both full platform lanes in CI #41116732941 (acceptance correctly skipped on diagnostic). The PR #92 documentation-index regression was reproduced in CI #41118003732 and fixed at `2490b27248d612904415240be8dbc656eee1da51`; append-only gate workflow ported at `6ffa5a6e2dd4c7f1f7f162921ab1481611b04093`. A historical regression overreached by rejecting the intentionally branch-restricted acceptance job; RED Ubuntu CI #41119009453 and previously validated narrow test correction ported at `a40b3186f38a52c3d66cda2e6b8b26cdd508417f`. Production-head CI #41119119763 was in progress at this checkpoint, **not** claimed green. The independent gate must fail closed while 13 PR #92 threads remain unresolved, three distinct exact-source code/architecture/security approvals are missing, Issue #73 lacks controlled physical NTFS sudden-power-loss evidence, and two distinct complete same-final-SHA runs and a genuinely persisted `ACCEPTED` are absent. PR #92 remains draft/unmerged; no T04 v0.12 merge or v0.13 release is authorized. Repository Issue #84 records source/CI provenance in comment #6532264975. - **T04-P2 literal JSON patch-key compatibility (30 October 2026):** PR #92 exact-source review on `952ae0dec1ae7924286c37d3567ec571689efea7` identified that internally generated patch paths rejected valid JSON keys including empty strings, LF, CR and NUL. Isolated **draft, do-not-merge** diagnostic [PR #119](https://github.com/eybersjp/development-kit/pull/119) reproduced the defect with adversarial update/deletion tests on **both Ubuntu and Windows** in [CI #41088497647](https://github.com/eybersjp/development-kit/actions/runs/41088497647) (RED). The narrow correction permits any literal string segment in JSON array paths while retaining type validation and own-property-safe patch writes. Diagnostic `51f80f691d485e47dd201a42a3ae64545d1771fb` passed **both full Ubuntu/Windows v0.13 validation jobs** in [CI #41088653146](https://github.com/eybersjp/development-kit/actions/runs/41088653146) (GREEN). Only runtime and test were ported to actual v0.13 PR #92 in `f35455dff057cd550eccc4d0100f54292b65d711` and `b6a25cbc510dd8011fc43319dc7927f71b2b42c2`; diagnostic workflow exception was **not** ported. This does not establish independent acceptance or NTFS physical power-loss durability. From 333d2f82c870563a7268a83df0f03217d7668e42 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 23:36:19 +0200 Subject: [PATCH 096/177] test(t04): bind both platform validation checkouts to exact reviewed PR head --- scripts/v013-t04-acceptance-ci-contract.test.mjs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs index 090dfb2dc..471322475 100644 --- a/scripts/v013-t04-acceptance-ci-contract.test.mjs +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -65,3 +65,13 @@ test('T04-A16 acceptance workflow reads exact PR head with least privilege', () assert.match(job, /actions: read/); assert.doesNotMatch(job, /contents: write|pull-requests: write|issues: write|actions: write/); }); + +test('T04-A17 both platform validations execute the immutable PR head, never a synthetic merge', () => { + const validate = workflow.split(/^ validate:\s*$/m)[1]?.split(/^ [a-z][\w-]*:\s*$/m)[0] ?? ''; + const checkout = validate.split(/ - name: Check out repository\s*\n/)[1]?.split(/ - name: /)[0] ?? ''; + assert.match(checkout, /uses: actions\/checkout@v7/); + assert.match(checkout, /ref: \$\{\{ github\.event\.pull_request\.head\.sha \|\| github\.sha \}\}/, + 'Both Ubuntu and Windows must validate the exact reviewed head, not refs/pull/N/merge'); + assert.match(checkout, /persist-credentials: false/, + 'Validation must not retain checkout credentials from untrusted PR source'); +}); From 7502e41d03497b7fd3b185018745bb6cef1f57a1 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 23:36:25 +0200 Subject: [PATCH 097/177] fix(t04): execute Ubuntu and Windows CI on immutable reviewed PR source --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c6c6b2c7..93b13735e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,9 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v7 From 0e351370e156765634683865cc1f57b8105d9bc3 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Fri, 30 Oct 2026 23:36:36 +0200 Subject: [PATCH 098/177] docs(t04): record test-first exact-head CI provenance correction and remaining gates --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index 91e7af7c9..4d4eae703 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-A17 exact-source CI provenance (30 October 2026; NOT ACCEPTED):** PR #92's `validate` matrix previously checked out GitHub's synthetic PR merge ref, not the immutable reviewed PR head. GitHub Actions PR #92 run #41127165486 checked out merge `25302739265ee4e4335ead869f81ecfefae96500` instead of PR head `f3bc71988cc8a472aaeabefc2397ebe5db4c1d85`; diagnostic PR #123 run #41127572518 checked out merge `0cc2e65` instead of that same head. Draft, do-not-merge diagnostic [PR #124](https://github.com/eybersjp/development-kit/pull/124) added the T04-A17 exact-checkout negative control; both full Ubuntu and Windows jobs reproduced its failure in [RED CI #41130880458](https://github.com/eybersjp/development-kit/actions/runs/41130880458). Narrow workflow fix sets `actions/checkout@v7` in the shared validation matrix to `${{ github.event.pull_request.head.sha || github.sha }}` and `persist-credentials: false`; the diagnostic exact-head `5fd3641078258be5897d4ca186ed346755ca6a97` passed **both complete Ubuntu and Windows v0.13 validation jobs** in [GREEN CI #41131224727](https://github.com/eybersjp/development-kit/actions/runs/41131224727), with job logs confirming checkout of `5fd3641` and T04-A17 PASS. Only the validated test and checkout fix were ported to actual integration PR #92 in `333d2f82c870563a7268a83df0f03217d7668e42` and `7502e41d03497b7fd3b185018745bb6cef1f57a1`; the diagnostic PR trigger was not ported. Fresh production-head CI is required. Previous same-SHA runs tested merge sources and must not be represented as two **exact-source** validation executions. Issue #73 physical NTFS power-loss evidence, 13 unresolved independent-review threads, three authentic exact-source approvals and persisted risk-3 ACCEPTED remain blockers. No merge or release is authorized. + - **T04-H56 pending-recovery snapshot-witness I/O fail-closed (30 October 2026; NOT ACCEPTED):** Actual integration draft PR #92 inherited a recovery path that treated `EACCES`/I/O failure reading the materialized snapshot witness as if it were a disposable invalid snapshot, permitting canonical ledger truncation/repair and pending-journal acknowledgement without inspecting a potentially newer witness. Isolated draft diagnostic [PR #122](https://github.com/eybersjp/development-kit/pull/122) used descriptor-relative-path-aware `T04-H56` fault injection and reproduced the missing fail-closed rejection on **Ubuntu and Windows** in [RED CI #41126196799](https://github.com/eybersjp/development-kit/actions/runs/41126196799). Narrow correction separates filesystem read errors from JSON/derived-schema corruption: only `ENOENT` permits absent-witness recovery; other read errors abort before any canonical mutation, preserving original ledger and pending journal. Diagnostic `ee0e5f69b9f619b87a694bb3f93d3790873323c2` passed both complete Ubuntu/Windows v0.13 compatibility lanes in [GREEN CI #41126337280](https://github.com/eybersjp/development-kit/actions/runs/41126337280). Only regression test `77bfac8f398b733f16ebf4ea55f272e3c96d0069` and runtime correction `a2dc909cb69f5e73acfd1a038f9c8272a7e2c94a` were ported to PR #92; diagnostic PR #122 must not merge. Production-head CI and exact-source independent reviews must be rerun; NTFS physical sudden-power-loss Issue #73, unresolved PR threads, and authentic persisted risk-3 ACCEPTED remain blockers. - **Risk-3 deterministic acceptance enforcement (30 October 2026, NOT ACCEPTED):** actual integration draft PR #92 now includes a distinct source-bound `t04_acceptance` GitHub Actions job depending on both complete Ubuntu/Windows validation jobs, with read-only authenticated review/CI provenance, pending receipts, physical NTFS evidence requirements, canonical `decideAcceptance` and uploaded verdict artifact only on genuine success. Diagnostic draft PR #121 first reproduced the absent gate in RED Ubuntu/Windows CI #41101076526; corrected diagnostic SHA `6e228fb02f29f07131f651fc4f7077ddc1f39ab6` passed both full platform lanes in CI #41116732941 (acceptance correctly skipped on diagnostic). The PR #92 documentation-index regression was reproduced in CI #41118003732 and fixed at `2490b27248d612904415240be8dbc656eee1da51`; append-only gate workflow ported at `6ffa5a6e2dd4c7f1f7f162921ab1481611b04093`. A historical regression overreached by rejecting the intentionally branch-restricted acceptance job; RED Ubuntu CI #41119009453 and previously validated narrow test correction ported at `a40b3186f38a52c3d66cda2e6b8b26cdd508417f`. Production-head CI #41119119763 was in progress at this checkpoint, **not** claimed green. The independent gate must fail closed while 13 PR #92 threads remain unresolved, three distinct exact-source code/architecture/security approvals are missing, Issue #73 lacks controlled physical NTFS sudden-power-loss evidence, and two distinct complete same-final-SHA runs and a genuinely persisted `ACCEPTED` are absent. PR #92 remains draft/unmerged; no T04 v0.12 merge or v0.13 release is authorized. Repository Issue #84 records source/CI provenance in comment #6532264975. From d0256f6f1915b98e5753f85000f3ec6e2d7fa9ff Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 02:46:06 +0200 Subject: [PATCH 099/177] fix(t04): require authenticated CI workflow and successful v0.13 steps for each validation receipt (A18) --- scripts/v013-t04-acceptance-gate.mjs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/scripts/v013-t04-acceptance-gate.mjs b/scripts/v013-t04-acceptance-gate.mjs index 39e6085d3..5dd063007 100644 --- a/scripts/v013-t04-acceptance-gate.mjs +++ b/scripts/v013-t04-acceptance-gate.mjs @@ -175,6 +175,10 @@ for (const id of validationRuns) { const run = await api('actions/runs/' + id); assert.equal(run.head_sha, reviewedCommit, 'Validation run is not exact-source'); assert.equal(run.event, 'pull_request'); + assert.equal(run.path, '.github/workflows/ci.yml', + 'Only the authoritative full CI workflow can count as risk-3 validation evidence'); + assert.equal(run.repository?.full_name, REPO, + 'Cross-repository or unrelated workflow evidence is not admissible'); assert.equal(run.status, 'completed'); // Diagnostic/production acceptance jobs may fail while validation succeeds: // verify both full platform jobs individually, not overall run conclusion. @@ -188,6 +192,22 @@ for (const id of validationRuns) { const matches = jobs.filter(job => job.name === 'validate (' + os + ')'); assert.equal(matches.length, 1, 'Missing unique full validation for ' + os); assert.equal(matches[0].conclusion, 'success', 'Full validation failed on ' + os); + // A separate workflow may reuse the validate job name without executing + // the v0.13 compatibility, source-security or acceptance negative controls. + // Require authenticated successful step-level witnesses on BOTH platforms. + for (const required of [ + 'Validate v0.13 State Engine V2 increment', + 'Validate v0.13 full compatibility regression', + 'Verify v0.13 risk-3 acceptance job contract (negative control)', + ]) { + const matchingSteps = (matches[0].steps || []).filter(step => step.name === required); + assert.equal(matchingSteps.length, 1, + 'Missing unique mandatory full-validation step on ' + os + ': ' + required); + for (const step of matchingSteps) { + assert.ok(step.conclusion === 'success', + 'Mandatory full-validation step did not pass on ' + os + ': ' + required); + } + } } } // The canonical Development Contract acceptance engine, not a bespoke green From d558d588a2796e9c230960d3338a7e65834e77ce Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 02:46:11 +0200 Subject: [PATCH 100/177] test(t04): enforce full-workflow provenance and step-success negative controls (A18) --- .../v013-t04-acceptance-ci-contract.test.mjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs index 471322475..f5684f4be 100644 --- a/scripts/v013-t04-acceptance-ci-contract.test.mjs +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -75,3 +75,21 @@ test('T04-A17 both platform validations execute the immutable PR head, never a s assert.match(checkout, /persist-credentials: false/, 'Validation must not retain checkout credentials from untrusted PR source'); }); + +test('T04-A18 acceptance gate binds validation receipts to authentic full CI workflow and mandatory steps', () => { + const gate = fs.readFileSync(path.join(root, 'scripts/v013-t04-acceptance-gate.mjs'), 'utf8'); + assert.match(gate, /run\.path, '\.github\/workflows\/ci\.yml'/, + 'same-SHA workflow run IDs must identify the authoritative full CI workflow, not a synthetic green workflow'); + assert.match(gate, /run\.repository\?\.full_name, REPO/, + 'cross-repository validation runs must never count toward source acceptance'); + for (const step of [ + 'Validate v0.13 State Engine V2 increment', + 'Validate v0.13 full compatibility regression', + 'Verify v0.13 risk-3 acceptance job contract (negative control)', + ]) { + assert.ok(gate.includes(step), + 'each OS validation must have a successful mandatory v0.13 step: ' + step); + } + assert.match(gate, /step\.conclusion === 'success'/, + 'the existence of a named validation step is insufficient without its successful conclusion'); +}); From 37b97ba7644888ad92371cde3261ae5062ffb8f3 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 02:46:31 +0200 Subject: [PATCH 101/177] docs(t04): record A18 red/green authenticated validation provenance and release boundaries --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index 4d4eae703..637e8e0fd 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-A18 authenticated validation workflow provenance (31 October 2026; NOT ACCEPTED):** Risk-3 gate previously accepted any two same-SHA GitHub PR workflow executions with jobs named `validate (ubuntu-latest)` and `validate (windows-latest)` even if a different workflow skipped mandatory v0.13 checks. Isolated draft, do-not-merge diagnostic [PR #126](https://github.com/eybersjp/development-kit/pull/126) added a fail-first contract test; **both Ubuntu and Windows** reproduced the missing workflow identity and mandatory step-success checks in [RED CI #41150895862](https://github.com/eybersjp/development-kit/actions/runs/41150895862). Narrow gate correction requires authenticated run `path` `.github/workflows/ci.yml`, repository `eybersjp/development-kit`, and successful mandatory State Engine V2, full compatibility regression and risk-3 contract steps in **each** OS job. Diagnostic SHA `20c8f6072f9d4f3898c8f96956d35c1953c8f9b0` passed both complete Ubuntu/Windows validation jobs in [GREEN CI #41151290304](https://github.com/eybersjp/development-kit/actions/runs/41151290304); diagnostic acceptance correctly skipped. Only the validated gate/test were ported to authoritative PR #92 in `d0256f6f1915b98e5753f85000f3ec6e2d7fa9ff` and `d558d588a2796e9c230960d3338a7e65834e77ce`; diagnostic PR #126 must not merge. Fresh two-run exact-final-head validation is required after this memory update. The 13 unresolved review threads, distinct exact-source code/architecture/security approvals, physical NTFS sudden-power-loss evidence under Issue #73, and authentic persisted `ACCEPTED` remain mandatory. Neither T04 nor diagnostics may merge without protected v0.13 release approval. + - **T04-A17 exact-source CI provenance (30 October 2026; NOT ACCEPTED):** PR #92's `validate` matrix previously checked out GitHub's synthetic PR merge ref, not the immutable reviewed PR head. GitHub Actions PR #92 run #41127165486 checked out merge `25302739265ee4e4335ead869f81ecfefae96500` instead of PR head `f3bc71988cc8a472aaeabefc2397ebe5db4c1d85`; diagnostic PR #123 run #41127572518 checked out merge `0cc2e65` instead of that same head. Draft, do-not-merge diagnostic [PR #124](https://github.com/eybersjp/development-kit/pull/124) added the T04-A17 exact-checkout negative control; both full Ubuntu and Windows jobs reproduced its failure in [RED CI #41130880458](https://github.com/eybersjp/development-kit/actions/runs/41130880458). Narrow workflow fix sets `actions/checkout@v7` in the shared validation matrix to `${{ github.event.pull_request.head.sha || github.sha }}` and `persist-credentials: false`; the diagnostic exact-head `5fd3641078258be5897d4ca186ed346755ca6a97` passed **both complete Ubuntu and Windows v0.13 validation jobs** in [GREEN CI #41131224727](https://github.com/eybersjp/development-kit/actions/runs/41131224727), with job logs confirming checkout of `5fd3641` and T04-A17 PASS. Only the validated test and checkout fix were ported to actual integration PR #92 in `333d2f82c870563a7268a83df0f03217d7668e42` and `7502e41d03497b7fd3b185018745bb6cef1f57a1`; the diagnostic PR trigger was not ported. Fresh production-head CI is required. Previous same-SHA runs tested merge sources and must not be represented as two **exact-source** validation executions. Issue #73 physical NTFS power-loss evidence, 13 unresolved independent-review threads, three authentic exact-source approvals and persisted risk-3 ACCEPTED remain blockers. No merge or release is authorized. - **T04-H56 pending-recovery snapshot-witness I/O fail-closed (30 October 2026; NOT ACCEPTED):** Actual integration draft PR #92 inherited a recovery path that treated `EACCES`/I/O failure reading the materialized snapshot witness as if it were a disposable invalid snapshot, permitting canonical ledger truncation/repair and pending-journal acknowledgement without inspecting a potentially newer witness. Isolated draft diagnostic [PR #122](https://github.com/eybersjp/development-kit/pull/122) used descriptor-relative-path-aware `T04-H56` fault injection and reproduced the missing fail-closed rejection on **Ubuntu and Windows** in [RED CI #41126196799](https://github.com/eybersjp/development-kit/actions/runs/41126196799). Narrow correction separates filesystem read errors from JSON/derived-schema corruption: only `ENOENT` permits absent-witness recovery; other read errors abort before any canonical mutation, preserving original ledger and pending journal. Diagnostic `ee0e5f69b9f619b87a694bb3f93d3790873323c2` passed both complete Ubuntu/Windows v0.13 compatibility lanes in [GREEN CI #41126337280](https://github.com/eybersjp/development-kit/actions/runs/41126337280). Only regression test `77bfac8f398b733f16ebf4ea55f272e3c96d0069` and runtime correction `a2dc909cb69f5e73acfd1a038f9c8272a7e2c94a` were ported to PR #92; diagnostic PR #122 must not merge. Production-head CI and exact-source independent reviews must be rerun; NTFS physical sudden-power-loss Issue #73, unresolved PR threads, and authentic persisted risk-3 ACCEPTED remain blockers. From 581133fd718088fb86306996b373c9cfe4b50d1b Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 05:29:30 +0200 Subject: [PATCH 102/177] test(T04-A19): cover revoked and stale effective role approvals --- .../v013-t04-acceptance-ci-contract.test.mjs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs index f5684f4be..9b0a5d3b2 100644 --- a/scripts/v013-t04-acceptance-ci-contract.test.mjs +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -93,3 +93,32 @@ test('T04-A18 acceptance gate binds validation receipts to authentic full CI wor assert.match(gate, /step\.conclusion === 'success'/, 'the existence of a named validation step is insufficient without its successful conclusion'); }); + +test('T04-A19 later review revocation or stale reapproval cannot be hidden by an earlier receipt', async () => { + const { assertCurrentRoleApproval } = await import('./v013-t04-review-provenance.mjs'); + const gate = fs.readFileSync(path.join(root, 'scripts/v013-t04-acceptance-gate.mjs'), 'utf8'); + assert.match(gate, /assertCurrentRoleApproval\(review, allSubmittedReviews, reviewedCommit\)/, + 'production acceptance must validate the latest effective GitHub review, not only the receipt ID'); + + const sha = 'a'.repeat(40); + const other = 'b'.repeat(40); + const approval = { id: 101, state: 'APPROVED', commit_id: sha, + user: { login: 'independent-code-reviewer' }, submitted_at: '2026-10-30T10:00:00Z' }; + const comment = { ...approval, id: 102, state: 'COMMENTED', + submitted_at: '2026-10-30T10:30:00Z' }; + const revoked = { ...approval, id: 103, state: 'CHANGES_REQUESTED', + submitted_at: '2026-10-30T11:00:00Z' }; + const staleReapproval = { ...approval, id: 104, commit_id: other, + submitted_at: '2026-10-30T12:00:00Z' }; + assert.doesNotThrow(() => assertCurrentRoleApproval(approval, [approval, comment], sha)); + assert.throws(() => assertCurrentRoleApproval(approval, [approval, revoked], sha), + /superseded|revoked|effective/i); + assert.throws(() => assertCurrentRoleApproval(approval, [approval, staleReapproval], sha), + /superseded|stale|effective/i); + assert.throws(() => assertCurrentRoleApproval(approval, + [{ ...approval, state: 'DISMISSED' }], sha), /approved|dismissed|effective/i); + assert.throws(() => assertCurrentRoleApproval(approval, [approval, { + ...approval, id: 105, state: 'CHANGES_REQUESTED', + submitted_at: '2026-10-30T13:00:00Z', + }], sha), /superseded|revoked|effective/i); +}); From 901f30c4d80bcef052954a7a46a1f18a6a344160 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 05:29:35 +0200 Subject: [PATCH 103/177] fix(T04-A19): enforce latest authenticated decisive review --- scripts/v013-t04-review-provenance.mjs | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 scripts/v013-t04-review-provenance.mjs diff --git a/scripts/v013-t04-review-provenance.mjs b/scripts/v013-t04-review-provenance.mjs new file mode 100644 index 000000000..78dc8f216 --- /dev/null +++ b/scripts/v013-t04-review-provenance.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; + +// GitHub's single-review endpoint can return an authentic APPROVED receipt +// even after that reviewer subsequently requested changes or reapproved a +// different source commit. Require the latest *decisive* submitted review +// from the same authenticated identity to be this exact source-bound receipt. +export function assertCurrentRoleApproval(approval, submittedReviews, reviewedCommit) { + assert.ok(approval && Number.isSafeInteger(approval.id) && approval.id > 0, + 'Authenticated role review ID is required'); + assert.equal(approval.state, 'APPROVED', 'Role review must be approved'); + assert.equal(approval.commit_id, reviewedCommit, 'Role approval source is stale'); + const login = approval.user?.login; + assert.ok(typeof login === 'string' && login.length > 0, + 'Role reviewer identity is missing'); + assert.ok(Array.isArray(submittedReviews), 'Authenticated PR review timeline is unavailable'); + const decisive = submittedReviews.filter(review => + review?.user?.login?.toLowerCase() === login.toLowerCase() && + ['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(review.state)); + assert.ok(decisive.length > 0, 'Role approval absent from authenticated review timeline'); + for (const review of decisive) { + assert.ok(Number.isSafeInteger(review.id) && review.id > 0 && + typeof review.submitted_at === 'string' && + Number.isFinite(Date.parse(review.submitted_at)), + 'Submitted role review lacks an authenticated ordering witness'); + } + decisive.sort((a, b) => + Date.parse(a.submitted_at) - Date.parse(b.submitted_at) || a.id - b.id); + const effective = decisive.at(-1); + assert.equal(effective.id, approval.id, + 'Role approval was superseded by a later decisive review'); + assert.equal(effective.state, 'APPROVED', + 'Effective role review is not approved'); + assert.equal(effective.commit_id, reviewedCommit, + 'Effective role approval source is stale'); +} From 7eb34f59044fe01e7847f0fcd6f653d2f76a5793 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 05:29:39 +0200 Subject: [PATCH 104/177] fix(T04-A19): reject superseded source-bound review receipts --- scripts/v013-t04-acceptance-gate.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/v013-t04-acceptance-gate.mjs b/scripts/v013-t04-acceptance-gate.mjs index 5dd063007..a1c510fff 100644 --- a/scripts/v013-t04-acceptance-gate.mjs +++ b/scripts/v013-t04-acceptance-gate.mjs @@ -11,6 +11,7 @@ import { createVerificationRecord, evaluateControlCoverage, getRunDirectory, persistVerificationRecord, persistControlManifest } from '../runtime/orchestration/evidence-store.mjs'; import { createReviewResult } from '../runtime/orchestration/review-result.mjs'; import { detectArchitectureDrift } from '../runtime/orchestration/architecture-drift.mjs'; +import { assertCurrentRoleApproval } from './v013-t04-review-provenance.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const REPO = 'eybersjp/development-kit'; @@ -87,6 +88,17 @@ assert.equal(pr.head.repo.full_name, REPO); // Distinct GitHub-authored, post-source reviews. Self-authored PASS strings are // never an approval. Each review must explicitly identify its role and SHA. +// Fetch all submitted PR reviews, including later revocations or source-stale +// reapprovals; the individual review endpoint alone cannot prove an approval +// is still the effective review from that independent identity. +const allSubmittedReviews = []; +for (let page = 1; ; page++) { + const batch = await api('pulls/92/reviews?per_page=100&page=' + page); + assert.ok(Array.isArray(batch), 'Authenticated PR review timeline is unavailable'); + allSubmittedReviews.push(...batch); + if (batch.length < 100) break; +} + const reviewers = new Set(); const authenticatedReviews = new Map(); const reviewIds = new Set(); @@ -103,6 +115,7 @@ for (const role of ROLES) { 'Author cannot independently approve their own code'); assert.ok(!reviewers.has(review.user.login), 'Distinct reviewer identity required for each role'); reviewers.add(review.user.login); + assertCurrentRoleApproval(review, allSubmittedReviews, reviewedCommit); authenticatedReviews.set(role, review); assert.ok((review.body || '').includes('DKF130-T04 ' + role + ' ' + reviewedCommit), 'Approval must explicitly bind role and exact source SHA'); From 81b6add1829568b0376c7e14c6dc5b939a1f3462 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 05:29:43 +0200 Subject: [PATCH 105/177] docs(T04-A19): document effective role review provenance --- .../scripts/v013-t04-review-provenance.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/03-reference/scripts/v013-t04-review-provenance.md diff --git a/docs/03-reference/scripts/v013-t04-review-provenance.md b/docs/03-reference/scripts/v013-t04-review-provenance.md new file mode 100644 index 000000000..eb0b429bd --- /dev/null +++ b/docs/03-reference/scripts/v013-t04-review-provenance.md @@ -0,0 +1,15 @@ +# v013-t04-review-provenance.mjs + +## Purpose + +`scripts/v013-t04-review-provenance.mjs` supplies the narrowly scoped authenticated-review effective-state guard used by the DKF130-T04 v0.13 risk-3 acceptance evaluator. + +## Review provenance contract + +A valid role receipt identifies a distinct authenticated GitHub `APPROVED` review bound to the immutable reviewed source commit. The single-review endpoint alone is insufficient: an earlier approved review may have been superseded by a later `CHANGES_REQUESTED`, `DISMISSED`, or approval on a different commit. + +`assertCurrentRoleApproval(approval, submittedReviews, reviewedCommit)` examines the authenticated, paginated pull-request review timeline for that reviewer. It ignores non-decisive `COMMENTED` submissions, requires valid submitted-at ordering witnesses, and fails closed unless the latest decisive review is the exact approved receipt for the exact reviewed commit. The caller must retrieve the review timeline through authenticated GitHub API calls and retain the independent role, distinct identity, zero unresolved threads, and physical NTFS evidence gates. + +## Validation + +`node --test scripts/v013-t04-acceptance-ci-contract.test.mjs` includes T04-A19 adversarial cases for revoked approvals, stale reapprovals, dismissed reviews, and harmless later comments. Both Ubuntu and Windows full CI jobs must pass on the same candidate commit. This helper never issues approvals, marks review threads resolved, merges PRs, or emits `ACCEPTED` by itself. From 7c1d04ee5cc690b9e1763791769c1f11aba2f736 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 05:29:47 +0200 Subject: [PATCH 106/177] docs(T04-A19): index role review provenance reference --- docs/SUMMARY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 7d6b7db91..dc62ebc84 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -197,6 +197,7 @@ * [phase05-execution-capsule-contract](03-reference/scripts/phase05-execution-capsule-contract.md) * [v013-state-engine-v2-contract](03-reference/scripts/v013-state-engine-v2-contract.md) * [v013-t04-acceptance-gate](03-reference/scripts/v013-t04-acceptance-gate.md) +* [v013-t04-review-provenance](03-reference/scripts/v013-t04-review-provenance.md) * [state-engine-v2-benchmark](03-reference/scripts/state-engine-v2-benchmark.md) * [t04-review-thread-blockers](03-reference/scripts/t04-review-thread-blockers.md) * [sync-plugin](03-reference/scripts/sync-plugin.md) From 63bef366cae67c71d075756be2b419a2bd468028 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 05:30:05 +0200 Subject: [PATCH 107/177] docs(T04-A19): record red/green CI and protected v0.13 integration evidence --- memory.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/memory.md b/memory.md index 637e8e0fd..6719c3993 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,10 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-A19 effective GitHub role-review provenance (31 October 2026; NOT ACCEPTED):** The risk-3 gate could authenticate an old `APPROVED` review receipt while ignoring a later `CHANGES_REQUESTED`, `DISMISSED`, or reapproval bound to a different source SHA from the same reviewer. Isolated draft diagnostic [PR #129](https://github.com/eybersjp/development-kit/pull/129) added adversarial T04-A19 tests on PR #92 source `37b97ba7644888ad92371cde3261ae5062ffb8f3`; **both Ubuntu and Windows** reproduced the missing effective-review guard in [RED CI #41163645280](https://github.com/eybersjp/development-kit/actions/runs/41163645280). A narrow authenticated, paginated PR review-timeline guard now requires the latest decisive review by each independent role identity to be the exact approved source-bound receipt; later comments do not revoke an approval. Documentation-index regression in the first candidate was corrected; diagnostic final SHA `74a5233c7a05c28b88d1e881e24c56615bccb48a` passed **both complete Ubuntu and Windows validation jobs** in [GREEN CI #41164008154](https://github.com/eybersjp/development-kit/actions/runs/41164008154); diagnostic acceptance correctly skipped. Only the validated test, helper, gate and required documentation were ported to authoritative PR #92 through `7c1d04ee5cc690b9e1763791769c1f11aba2f736`. Fresh integration-head CI and exact-source reviews are required. PR #129 remains diagnostic and must not merge. Thirteen unresolved PR #92 review threads, three independent code/architecture/security approvals, controlled physical NTFS power-loss Issue #73 evidence, two full same-final-source validation runs and an authentic persisted `ACCEPTED` verdict remain mandatory; no T04 merge/release is authorized. + + + - **T04-A18 authenticated validation workflow provenance (31 October 2026; NOT ACCEPTED):** Risk-3 gate previously accepted any two same-SHA GitHub PR workflow executions with jobs named `validate (ubuntu-latest)` and `validate (windows-latest)` even if a different workflow skipped mandatory v0.13 checks. Isolated draft, do-not-merge diagnostic [PR #126](https://github.com/eybersjp/development-kit/pull/126) added a fail-first contract test; **both Ubuntu and Windows** reproduced the missing workflow identity and mandatory step-success checks in [RED CI #41150895862](https://github.com/eybersjp/development-kit/actions/runs/41150895862). Narrow gate correction requires authenticated run `path` `.github/workflows/ci.yml`, repository `eybersjp/development-kit`, and successful mandatory State Engine V2, full compatibility regression and risk-3 contract steps in **each** OS job. Diagnostic SHA `20c8f6072f9d4f3898c8f96956d35c1953c8f9b0` passed both complete Ubuntu/Windows validation jobs in [GREEN CI #41151290304](https://github.com/eybersjp/development-kit/actions/runs/41151290304); diagnostic acceptance correctly skipped. Only the validated gate/test were ported to authoritative PR #92 in `d0256f6f1915b98e5753f85000f3ec6e2d7fa9ff` and `d558d588a2796e9c230960d3338a7e65834e77ce`; diagnostic PR #126 must not merge. Fresh two-run exact-final-head validation is required after this memory update. The 13 unresolved review threads, distinct exact-source code/architecture/security approvals, physical NTFS sudden-power-loss evidence under Issue #73, and authentic persisted `ACCEPTED` remain mandatory. Neither T04 nor diagnostics may merge without protected v0.13 release approval. - **T04-A17 exact-source CI provenance (30 October 2026; NOT ACCEPTED):** PR #92's `validate` matrix previously checked out GitHub's synthetic PR merge ref, not the immutable reviewed PR head. GitHub Actions PR #92 run #41127165486 checked out merge `25302739265ee4e4335ead869f81ecfefae96500` instead of PR head `f3bc71988cc8a472aaeabefc2397ebe5db4c1d85`; diagnostic PR #123 run #41127572518 checked out merge `0cc2e65` instead of that same head. Draft, do-not-merge diagnostic [PR #124](https://github.com/eybersjp/development-kit/pull/124) added the T04-A17 exact-checkout negative control; both full Ubuntu and Windows jobs reproduced its failure in [RED CI #41130880458](https://github.com/eybersjp/development-kit/actions/runs/41130880458). Narrow workflow fix sets `actions/checkout@v7` in the shared validation matrix to `${{ github.event.pull_request.head.sha || github.sha }}` and `persist-credentials: false`; the diagnostic exact-head `5fd3641078258be5897d4ca186ed346755ca6a97` passed **both complete Ubuntu and Windows v0.13 validation jobs** in [GREEN CI #41131224727](https://github.com/eybersjp/development-kit/actions/runs/41131224727), with job logs confirming checkout of `5fd3641` and T04-A17 PASS. Only the validated test and checkout fix were ported to actual integration PR #92 in `333d2f82c870563a7268a83df0f03217d7668e42` and `7502e41d03497b7fd3b185018745bb6cef1f57a1`; the diagnostic PR trigger was not ported. Fresh production-head CI is required. Previous same-SHA runs tested merge sources and must not be represented as two **exact-source** validation executions. Issue #73 physical NTFS power-loss evidence, 13 unresolved independent-review threads, three authentic exact-source approvals and persisted risk-3 ACCEPTED remain blockers. No merge or release is authorized. From 106847dd1afb38b29fbe419bacf292753d54e5ba Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 07:17:13 +0200 Subject: [PATCH 108/177] fix(T04): preserve active writer temporary files during orphan cleanup (validated #130) --- runtime/orchestration/state-engine-v2.mjs | 28 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index c5ba06b15..4d791f2ee 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -1843,12 +1843,32 @@ export function cleanupOrphanedStateTemps(rootDir = process.cwd()) { if (!fs.existsSync(paths.stateRoot)) return []; const removed = []; for (const name of fs.readdirSync(paths.stateRoot)) { - if (!name.includes('.tmp-')) continue; + // Only internal publication stages may be garbage-collected. In + // particular, never delete a live writer's temporary file: a concurrent + // atomic rename would then fail after its bytes were already fsynced. + if (!/^(?:events\.jsonl|snapshot\.json|index\.db|schema\.json|pending-commit\.json)\.tmp-/.test(name)) continue; + const owner = /\.tmp-([1-9][0-9]*)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.exec(name); + if (owner) { + const pid = Number(owner[1]); + if (!Number.isSafeInteger(pid)) continue; + try { + process.kill(pid, 0); + continue; // Owner exists: do not interfere with its publication. + } catch (error) { + if (error.code !== 'ESRCH') continue; // Unknown/denied = live. + } + } const candidate = path.join(paths.stateRoot, name); - if (fs.statSync(candidate).isFile()) { - fs.unlinkSync(candidate); - removed.push(candidate); + let stat; + try { stat = fs.lstatSync(candidate); } + catch (error) { + if (error.code === 'ENOENT') continue; + throw error; } + // Hardlinked bootstrap witnesses require their own validated recovery. + if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) continue; + fs.unlinkSync(candidate); + removed.push(candidate); } return removed; } From dddf27d18a8533a5f3541339bffa1958f6d98e25 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 07:17:20 +0200 Subject: [PATCH 109/177] test(T04): retain active writer staging and clean abandoned temporary file --- scripts/state-engine-v2.test.mjs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/state-engine-v2.test.mjs b/scripts/state-engine-v2.test.mjs index 4e43e0669..ae7c93722 100644 --- a/scripts/state-engine-v2.test.mjs +++ b/scripts/state-engine-v2.test.mjs @@ -328,6 +328,23 @@ test('interrupted temporary state files cannot advance canonical history', (t) = assert.equal(fs.readFileSync(paths.events, 'utf8'), before); }); +test('T04 concurrent temporary cleanup preserves an active writer staging file', (t) => { + const rootDir = tempProject(t); + ensureStateEngineLayout(rootDir); + const paths = getStateEnginePaths(rootDir); + const liveStage = path.join(paths.stateRoot, 'events.jsonl.tmp-' + process.pid + '-00000000-0000-4000-8000-000000000001'); + const staleStage = path.join(paths.stateRoot, 'events.jsonl.tmp-interrupted'); + fs.writeFileSync(liveStage, 'active writer staging data'); + fs.writeFileSync(staleStage, 'abandoned data'); + + const removed = cleanupOrphanedStateTemps(rootDir); + assert.equal(fs.readFileSync(liveStage, 'utf8'), 'active writer staging data', + 'cleanup must not unlink the temporary file of a still-running writer'); + assert.ok(!removed.includes(liveStage)); + assert.ok(removed.includes(staleStage)); + assert.equal(fs.existsSync(staleStage), false); +}); + test('fresh Autopilot projects use State Engine V2 without legacy revision micro-files', (t) => { const rootDir = tempProject(t); const first = workflowState(1); From d405dc7fa7abb18d8bc59eee0c816b00e7454b79 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 07:17:29 +0200 Subject: [PATCH 110/177] docs(T04-A20): record red/green concurrency cleanup evidence and v0.13 port --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index 6719c3993..f1333b436 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-A20 active-writer temporary-file cleanup race (31 October 2026; NOT ACCEPTED):** `cleanupOrphanedStateTemps()` removed any file containing `.tmp-`, including a concurrently active writer's fsynced `events.jsonl.tmp--` stage, so publication could fail after preparation. Isolated draft diagnostic [PR #130](https://github.com/eybersjp/development-kit/pull/130) added an active-PID staging regression; both Ubuntu and Windows reproduced the defect in [RED CI #41171427038](https://github.com/eybersjp/development-kit/actions/runs/41171427038). The first implementation failed the existing abandoned-temp regression because of over-escaped regexes ([CI #41171525251](https://github.com/eybersjp/development-kit/actions/runs/41171525251)); corrected exact diagnostic SHA `54162254d1a7c57c5d807f2385da760bed91a9b9` passed complete Ubuntu and Windows validation in [GREEN CI #41171601007](https://github.com/eybersjp/development-kit/actions/runs/41171601007). The narrowly scoped source and regression were ported to authoritative draft PR #92 in commits `106847dd1afb38b29fbe419bacf292753d54e5ba` and `dddf27d18a8533a5f3541339bffa1958f6d98e25`. Cleanup now retains live or unknown PID-owned stages, restricts deletions to internal temporary names, uses lstat and preserves hardlink witnesses. Production-head CI, independent review, physical NTFS power-loss evidence, unresolved threads and risk-3 ACCEPTED are still pending. PR #130 remains unmerged diagnostic only. + - **T04-A19 effective GitHub role-review provenance (31 October 2026; NOT ACCEPTED):** The risk-3 gate could authenticate an old `APPROVED` review receipt while ignoring a later `CHANGES_REQUESTED`, `DISMISSED`, or reapproval bound to a different source SHA from the same reviewer. Isolated draft diagnostic [PR #129](https://github.com/eybersjp/development-kit/pull/129) added adversarial T04-A19 tests on PR #92 source `37b97ba7644888ad92371cde3261ae5062ffb8f3`; **both Ubuntu and Windows** reproduced the missing effective-review guard in [RED CI #41163645280](https://github.com/eybersjp/development-kit/actions/runs/41163645280). A narrow authenticated, paginated PR review-timeline guard now requires the latest decisive review by each independent role identity to be the exact approved source-bound receipt; later comments do not revoke an approval. Documentation-index regression in the first candidate was corrected; diagnostic final SHA `74a5233c7a05c28b88d1e881e24c56615bccb48a` passed **both complete Ubuntu and Windows validation jobs** in [GREEN CI #41164008154](https://github.com/eybersjp/development-kit/actions/runs/41164008154); diagnostic acceptance correctly skipped. Only the validated test, helper, gate and required documentation were ported to authoritative PR #92 through `7c1d04ee5cc690b9e1763791769c1f11aba2f736`. Fresh integration-head CI and exact-source reviews are required. PR #129 remains diagnostic and must not merge. Thirteen unresolved PR #92 review threads, three independent code/architecture/security approvals, controlled physical NTFS power-loss Issue #73 evidence, two full same-final-source validation runs and an authentic persisted `ACCEPTED` verdict remain mandatory; no T04 merge/release is authorized. From 1f5a4f374405f39cace2d101125f9f1288e0c90f Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 08:12:57 +0200 Subject: [PATCH 111/177] fix(t04): persist ACCEPTED verdict artifact fail-closed on exact v0.13 head --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93b13735e..59d48b536 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,9 +170,11 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: node scripts/v013-t04-acceptance-gate.mjs - name: Preserve canonical T04 verdict evidence - if: always() + if: ${{ success() }} uses: actions/upload-artifact@v4 with: - name: dkf-v013-t04-acceptance-evidence + name: dkf-v013-t04-acceptance-evidence-${{ github.event.pull_request.head.sha }} path: .development-kit/acceptance/DKF130-T04.json - if-no-files-found: ignore + if-no-files-found: error + include-hidden-files: true + retention-days: 90 From 089f30b6ad4a89f2c6e9cc82806f31f92c7b42a0 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 08:13:00 +0200 Subject: [PATCH 112/177] test(t04): enforce authenticated acceptance artifact persistence and hidden-file inclusion --- .../v013-t04-acceptance-ci-contract.test.mjs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs index 9b0a5d3b2..1253e19c5 100644 --- a/scripts/v013-t04-acceptance-ci-contract.test.mjs +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -122,3 +122,24 @@ test('T04-A19 later review revocation or stale reapproval cannot be hidden by an submitted_at: '2026-10-30T13:00:00Z', }], sha), /superseded|revoked|effective/i); }); + + +test('T04-A21 ACCEPTED verdict must be uploaded durably, only after a successful gate', () => { + const job = workflow.split(/^ t04_acceptance:\s*$/m)[1]?.split(/^ [a-z][\w-]*:\s*$/m)[0] ?? ''; + const upload = job.split(/ - name: Preserve canonical T04 verdict evidence\s*\n/)[1] + ?.split(/ - name: /)[0] ?? ''; + assert.ok(upload, 'T04 acceptance must persist its verdict outside the ephemeral runner'); + assert.match(upload, /if: \$\{\{ success\(\) \}\}/, + 'A failed acceptance gate must never upload a purported ACCEPTED verdict'); + assert.match(upload, /uses: actions\/upload-artifact@v4/, + 'Canonical ACCEPTED record must be retained as a GitHub Actions artifact'); + assert.match(upload, /name: dkf-v013-t04-acceptance-evidence-\$\{\{ github\.event\.pull_request\.head\.sha \}\}/, + 'Verdict artifact must identify its exact immutable reviewed source'); + assert.match(upload, /path: \.development-kit\/acceptance\/DKF130-T04\.json/, + 'Upload only the canonical deterministic Development Contract verdict'); + assert.match(upload, /if-no-files-found: error/, + 'Missing canonical verdict must fail the acceptance job, never silently pass'); + assert.match(upload, /include-hidden-files: true/, + 'The canonical verdict lives in a hidden .development-kit directory and must be included'); + assert.doesNotMatch(upload, /if-no-files-found: ignore/); +}); From cade28fe83cc9f5bf6aec5846519b062772fde79 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 08:13:09 +0200 Subject: [PATCH 113/177] docs(t04): record A21 red/green CI and safe v0.13 artifact provenance boundary --- memory.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/memory.md b/memory.md index f1333b436..1a062509e 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,9 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) + +- **T04-A21 fail-closed persisted ACCEPTED artifact (31 October 2026; NOT ACCEPTED):** The v0.13 `t04_acceptance` workflow previously used `if: always()` and `if-no-files-found: ignore` for `.development-kit/acceptance/DKF130-T04.json`. A successful deterministic gate could therefore finish without retaining the canonical verdict; the hidden `.development-kit` directory also requires explicit artifact inclusion. Test-first draft diagnostic [PR #131](https://github.com/eybersjp/development-kit/pull/131) reproduced T04-A21 on **both full Ubuntu and Windows** in [RED CI #41175009428](https://github.com/eybersjp/development-kit/actions/runs/41175009428). Diagnostic commit `532181357ecef71777e49b62992d1372c8357c65` passed **both full platform validation jobs** in [GREEN CI #41175217226](https://github.com/eybersjp/development-kit/actions/runs/41175217226). Only the validated workflow and negative-control test were ported to authoritative draft PR #92 in `1f5a4f374405f39cace2d101125f9f1288e0c90f` and `089f30b6ad4a89f2c6e9cc82806f31f92c7b42a0`. Artifact upload now requires a successful gate, an exact-head name, explicit hidden-file inclusion, and failure if the verdict file is absent; the artifact is retained for 90 days and must be preserved in approved release evidence thereafter. **No ACCEPTED artifact exists yet**: distinct independent exact-source code/architecture/security approvals, 13 review-thread dispositions, controlled NTFS physical power-loss evidence (Issue #73), two complete final-head validation runs and the authentic persisted verdict remain required. Diagnostic PR #131 must not merge; neither PR #92 nor historical T04 branches may merge without protected v0.13 approval. + - **T04-A20 active-writer temporary-file cleanup race (31 October 2026; NOT ACCEPTED):** `cleanupOrphanedStateTemps()` removed any file containing `.tmp-`, including a concurrently active writer's fsynced `events.jsonl.tmp--` stage, so publication could fail after preparation. Isolated draft diagnostic [PR #130](https://github.com/eybersjp/development-kit/pull/130) added an active-PID staging regression; both Ubuntu and Windows reproduced the defect in [RED CI #41171427038](https://github.com/eybersjp/development-kit/actions/runs/41171427038). The first implementation failed the existing abandoned-temp regression because of over-escaped regexes ([CI #41171525251](https://github.com/eybersjp/development-kit/actions/runs/41171525251)); corrected exact diagnostic SHA `54162254d1a7c57c5d807f2385da760bed91a9b9` passed complete Ubuntu and Windows validation in [GREEN CI #41171601007](https://github.com/eybersjp/development-kit/actions/runs/41171601007). The narrowly scoped source and regression were ported to authoritative draft PR #92 in commits `106847dd1afb38b29fbe419bacf292753d54e5ba` and `dddf27d18a8533a5f3541339bffa1958f6d98e25`. Cleanup now retains live or unknown PID-owned stages, restricts deletions to internal temporary names, uses lstat and preserves hardlink witnesses. Production-head CI, independent review, physical NTFS power-loss evidence, unresolved threads and risk-3 ACCEPTED are still pending. PR #130 remains unmerged diagnostic only. - **T04-A19 effective GitHub role-review provenance (31 October 2026; NOT ACCEPTED):** The risk-3 gate could authenticate an old `APPROVED` review receipt while ignoring a later `CHANGES_REQUESTED`, `DISMISSED`, or reapproval bound to a different source SHA from the same reviewer. Isolated draft diagnostic [PR #129](https://github.com/eybersjp/development-kit/pull/129) added adversarial T04-A19 tests on PR #92 source `37b97ba7644888ad92371cde3261ae5062ffb8f3`; **both Ubuntu and Windows** reproduced the missing effective-review guard in [RED CI #41163645280](https://github.com/eybersjp/development-kit/actions/runs/41163645280). A narrow authenticated, paginated PR review-timeline guard now requires the latest decisive review by each independent role identity to be the exact approved source-bound receipt; later comments do not revoke an approval. Documentation-index regression in the first candidate was corrected; diagnostic final SHA `74a5233c7a05c28b88d1e881e24c56615bccb48a` passed **both complete Ubuntu and Windows validation jobs** in [GREEN CI #41164008154](https://github.com/eybersjp/development-kit/actions/runs/41164008154); diagnostic acceptance correctly skipped. Only the validated test, helper, gate and required documentation were ported to authoritative PR #92 through `7c1d04ee5cc690b9e1763791769c1f11aba2f736`. Fresh integration-head CI and exact-source reviews are required. PR #129 remains diagnostic and must not merge. Thirteen unresolved PR #92 review threads, three independent code/architecture/security approvals, controlled physical NTFS power-loss Issue #73 evidence, two full same-final-source validation runs and an authentic persisted `ACCEPTED` verdict remain mandatory; no T04 merge/release is authorized. From e11da547b776c2cf306974535d1fd20f5ed6f7bb Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 08:31:51 +0200 Subject: [PATCH 114/177] test(t04): preserve JSON-only canonical event semantics --- scripts/state-engine-v2.test.mjs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/scripts/state-engine-v2.test.mjs b/scripts/state-engine-v2.test.mjs index ae7c93722..5c0336fca 100644 --- a/scripts/state-engine-v2.test.mjs +++ b/scripts/state-engine-v2.test.mjs @@ -617,3 +617,26 @@ test('T04-P2 JSON object keys including empty, control and Unicode strings survi assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); } }); + +test('T04-SEC-JSON-01 rejects non-lossless JSON state before canonical commit', (t) => { + const invalid = [ + ['Map', new Map([['secret', 'must-not-disappear']])], + ['Set', new Set(['must-not-disappear'])], + ['Date', new Date('2026-10-31T00:00:00.000Z')], + ['undefined', undefined], + ['NaN', Number.NaN], + ['Infinity', Number.POSITIVE_INFINITY], + ]; + for (const [label, value] of invalid) { + const rootDir = tempProject(t, 'dk-v13-json-lossless-'); + assert.throws(() => appendEntityState({ + rootDir, entityType: 'generic-state', entityId: 'json-regression', + state: { stateRevision: 1, metadata: { value } }, + actorClass: 'json-regression', + }), /JSON-serializable|JSON-compatible|lossless/i, + 'Reject ' + label + ' instead of acknowledging a lossy event'); + const paths = getStateEnginePaths(rootDir); + assert.equal(fs.existsSync(paths.events), false, + label + ' must not be acknowledged or committed to the canonical ledger'); + } +}); From 5d39940177330274ba6e37033e1c316d08263487 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 08:31:54 +0200 Subject: [PATCH 115/177] fix(t04): reject lossy JSON event values before commit --- runtime/orchestration/state-engine-v2.mjs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 4d791f2ee..c406374a2 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -2,6 +2,7 @@ import crypto from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { isDeepStrictEqual } from 'node:util'; export const STATE_ENGINE_SCHEMA_VERSION = '2.0.0'; export const STATE_EVENT_SCHEMA_VERSION = '1.0.0'; @@ -925,10 +926,17 @@ function normalizePathSegments(value) { function cloneSerializable(value, label) { try { const cloned = structuredClone(value); - JSON.stringify(cloned); + const encoded = JSON.stringify(cloned); + // JSON.stringify silently discards Map/Set contents and undefined object + // fields, and rewrites Date/non-finite numbers. A successful stringify + // alone cannot establish that an acknowledged event is replayable without + // semantic data loss. Reject before publishing any canonical bytes. + if (typeof encoded !== 'string' || !isDeepStrictEqual(cloned, JSON.parse(encoded))) { + throw new StateEngineError(`${label} must be lossless JSON-serializable data`); + } return cloned; } catch { - throw new StateEngineError(`${label} must be JSON-serializable`); + throw new StateEngineError(`${label} must be lossless JSON-serializable data`); } } From c0ba032e6fa6607c9a0aa288bc7c5fb1d7349aea Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 08:32:02 +0200 Subject: [PATCH 116/177] docs(t04): record JSON lossless red/green evidence and unaccepted release boundaries --- memory.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/memory.md b/memory.md index 1a062509e..2ff59ab99 100644 --- a/memory.md +++ b/memory.md @@ -12,6 +12,9 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-SEC-JSON-01 lossless canonical JSON boundary (31 October 2026; NOT ACCEPTED):** `cloneSerializable()` previously accepted Map, Set, Date, undefined object values and non-finite numbers because successful `JSON.stringify()` was mistaken for lossless serializability. An acknowledged V2 event could therefore silently discard or transform state data. Isolated **draft, DO NOT MERGE** diagnostic [PR #132](https://github.com/eybersjp/development-kit/pull/132) introduced the negative-control test first at `3ae982c72fbee77bf38768f813e80547cf36a623`; both Ubuntu and Windows `Validate v0.13 State Engine V2 increment` failed as expected in [RED CI #41176319558](https://github.com/eybersjp/development-kit/actions/runs/41176319558) (first failure: Map was acknowledged without throwing). Narrow correction uses Node `isDeepStrictEqual` against the canonical JSON parse round-trip and rejects lossy state before ledger publication. Diagnostic SHA `728291c317eb7b009a1c8164437b7869b279052f` passed **both complete Ubuntu and Windows v0.13 validation jobs** in [GREEN CI #41176399858](https://github.com/eybersjp/development-kit/actions/runs/41176399858). Ported only the validated regression and runtime change to authoritative draft PR #92 in `e11da547b776c2cf306974535d1fd20f5ed6f7bb` and `5d39940177330274ba6e37033e1c316d08263487`. This memory update changes the exact integration SHA; fresh cross-platform validation and independent exact-source review are required. **Still NOT ACCEPTED:** 13 PR #92 review threads, distinct code/architecture/security approvals, controlled physical NTFS power-loss Issue #73 evidence, two complete identical-final-SHA CI executions and persisted authentic verdict. No merges, waivers or v0.12 T04 inclusion. + + - **T04-A21 fail-closed persisted ACCEPTED artifact (31 October 2026; NOT ACCEPTED):** The v0.13 `t04_acceptance` workflow previously used `if: always()` and `if-no-files-found: ignore` for `.development-kit/acceptance/DKF130-T04.json`. A successful deterministic gate could therefore finish without retaining the canonical verdict; the hidden `.development-kit` directory also requires explicit artifact inclusion. Test-first draft diagnostic [PR #131](https://github.com/eybersjp/development-kit/pull/131) reproduced T04-A21 on **both full Ubuntu and Windows** in [RED CI #41175009428](https://github.com/eybersjp/development-kit/actions/runs/41175009428). Diagnostic commit `532181357ecef71777e49b62992d1372c8357c65` passed **both full platform validation jobs** in [GREEN CI #41175217226](https://github.com/eybersjp/development-kit/actions/runs/41175217226). Only the validated workflow and negative-control test were ported to authoritative draft PR #92 in `1f5a4f374405f39cace2d101125f9f1288e0c90f` and `089f30b6ad4a89f2c6e9cc82806f31f92c7b42a0`. Artifact upload now requires a successful gate, an exact-head name, explicit hidden-file inclusion, and failure if the verdict file is absent; the artifact is retained for 90 days and must be preserved in approved release evidence thereafter. **No ACCEPTED artifact exists yet**: distinct independent exact-source code/architecture/security approvals, 13 review-thread dispositions, controlled NTFS physical power-loss evidence (Issue #73), two complete final-head validation runs and the authentic persisted verdict remain required. Diagnostic PR #131 must not merge; neither PR #92 nor historical T04 branches may merge without protected v0.13 approval. - **T04-A20 active-writer temporary-file cleanup race (31 October 2026; NOT ACCEPTED):** `cleanupOrphanedStateTemps()` removed any file containing `.tmp-`, including a concurrently active writer's fsynced `events.jsonl.tmp--` stage, so publication could fail after preparation. Isolated draft diagnostic [PR #130](https://github.com/eybersjp/development-kit/pull/130) added an active-PID staging regression; both Ubuntu and Windows reproduced the defect in [RED CI #41171427038](https://github.com/eybersjp/development-kit/actions/runs/41171427038). The first implementation failed the existing abandoned-temp regression because of over-escaped regexes ([CI #41171525251](https://github.com/eybersjp/development-kit/actions/runs/41171525251)); corrected exact diagnostic SHA `54162254d1a7c57c5d807f2385da760bed91a9b9` passed complete Ubuntu and Windows validation in [GREEN CI #41171601007](https://github.com/eybersjp/development-kit/actions/runs/41171601007). The narrowly scoped source and regression were ported to authoritative draft PR #92 in commits `106847dd1afb38b29fbe419bacf292753d54e5ba` and `dddf27d18a8533a5f3541339bffa1958f6d98e25`. Cleanup now retains live or unknown PID-owned stages, restricts deletions to internal temporary names, uses lstat and preserves hardlink witnesses. Production-head CI, independent review, physical NTFS power-loss evidence, unresolved threads and risk-3 ACCEPTED are still pending. PR #130 remains unmerged diagnostic only. From 2c8f9cc024768213d6d66ce650623daeead7336c Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 09:28:05 +0200 Subject: [PATCH 117/177] test(t04): reproduce hard-link swap at ledger tail repair descriptor open --- scripts/state-engine-v2-hardening.test.mjs | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index 7eb4bd971..bcf9ee139 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -1154,3 +1154,52 @@ test('T04-H56 pending recovery preserves canonical evidence when snapshot witnes assert.equal(fs.existsSync(paths.pending), true, 'Failed witness read must retain the pending journal for operator recovery'); }); + +/** + * T04-H57: a canonical ledger may be swapped for an external hard link + * after the recovery preflight but before the repair opens it for mutation. + * Both newline completion and torn-tail truncation must fail closed. + */ +for (const repairMode of ['missing-terminal-newline', 'torn-trailing-json']) { + test('T04-H57 ledger tail repair rejects a swapped hard link (' + repairMode + ')', (t) => { + const rootDir = tempProject(t); + const outsideRoot = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const baseline = fs.readFileSync(paths.events, 'utf8'); + const damaged = repairMode === 'missing-terminal-newline' + ? baseline.slice(0, -1) + : baseline + '{"incomplete":'; + fs.writeFileSync(paths.events, damaged, 'utf8'); + const outsideFile = path.join(outsideRoot, 'unrelated-evidence.jsonl'); + fs.writeFileSync(outsideFile, damaged, 'utf8'); + + const originalOpen = fs.openSync; + let swaps = 0; + fs.openSync = function injectHardLinkAtRepairOpen(target, flags, ...args) { + const writeRepair = flags === 'r+' || + (typeof flags === 'number' && + (flags & fs.constants.O_RDWR) === fs.constants.O_RDWR); + if (target === paths.events && writeRepair) { + swaps += 1; + fs.unlinkSync(paths.events); + fs.linkSync(outsideFile, paths.events); + } + return originalOpen.call(this, target, flags, ...args); + }; + try { + assert.throws( + () => loadCanonicalEvents(rootDir), + /canonical ledger|hard.?link|integrity|identity|unsafe/i, + 'A post-preflight hard-link substitution must be rejected before repair writes', + ); + } finally { + fs.openSync = originalOpen; + } + assert.equal(swaps, 1, 'Fault injection must intercept the repair descriptor open'); + assert.equal(fs.readFileSync(outsideFile, 'utf8'), damaged, + 'Recovery must never append to or truncate unrelated external evidence'); + assert.equal(fs.readFileSync(paths.events, 'utf8'), damaged, + 'Rejected repair must not alter the substituted canonical path'); + }); +} From 4e969e2578b8c42ddc05c06ed304fae55baed78e Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 09:29:12 +0200 Subject: [PATCH 118/177] fix(t04): pin and verify ledger tail repair descriptor before mutation --- runtime/orchestration/state-engine-v2.mjs | 52 ++++++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index c406374a2..0b79139ff 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -828,7 +828,53 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { throw new StateEngineError('Pending commit remains unresolved during trailing-ledger recovery'); } + // Record the canonical inode before reading repair input. A path swapped + // after layout preflight must never become the target of truncation/append. + const initialEntry = fs.lstatSync(paths.events, { bigint: true }); + if (!initialEntry.isFile() || initialEntry.isSymbolicLink() || initialEntry.nlink !== 1n) { + throw new StateEngineError('Canonical ledger tail repair requires a single-link regular file'); + } const content = fs.readFileSync(paths.events, 'utf8'); + const expectedBytes = Buffer.from(content, 'utf8'); + + function verifyCanonicalTailRepairFd(fd) { + const opened = fs.fstatSync(fd, { bigint: true }); + const entry = fs.lstatSync(paths.events, { bigint: true }); + if (!opened.isFile() || !entry.isFile() || entry.isSymbolicLink() || + opened.nlink !== 1n || entry.nlink !== 1n || + opened.dev !== entry.dev || opened.ino !== entry.ino || + opened.dev !== initialEntry.dev || opened.ino !== initialEntry.ino) { + throw new StateEngineError('Canonical ledger tail repair descriptor identity or hard-link integrity failed'); + } + return opened; + } + + function openVerifiedCanonicalTailRepairFd() { + const flags = fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW ?? 0); + const fd = fs.openSync(paths.events, flags); + try { + const opened = verifyCanonicalTailRepairFd(fd); + if (opened.size !== BigInt(expectedBytes.length)) { + throw new StateEngineError('Canonical ledger tail repair bytes changed before descriptor open'); + } + const observed = Buffer.alloc(expectedBytes.length); + let offset = 0; + while (offset < observed.length) { + const count = fs.readSync(fd, observed, offset, observed.length - offset, offset); + if (count <= 0) { + throw new StateEngineError('Canonical ledger tail repair descriptor read was incomplete'); + } + offset += count; + } + if (!observed.equals(expectedBytes)) { + throw new StateEngineError('Canonical ledger tail repair descriptor contents changed before mutation'); + } + return fd; + } catch (error) { + fs.closeSync(fd); + throw error; + } + } if (!content || content.endsWith('\n')) return parseLedger(content); const boundary = content.lastIndexOf('\n'); @@ -842,12 +888,13 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { try { const event = JSON.parse(tail); validateStateEvent(event, previous); - const fd = fs.openSync(paths.events, 'r+'); + const fd = openVerifiedCanonicalTailRepairFd(); try { const stat = fs.fstatSync(fd); if (!stat.isFile()) throw new StateEngineError('Canonical ledger repair target is not a regular file'); fs.writeSync(fd, Buffer.from('\n'), 0, 1, Buffer.byteLength(content, 'utf8')); fs.fsyncSync(fd); + verifyCanonicalTailRepairFd(fd); } finally { fs.closeSync(fd); } @@ -883,10 +930,11 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { } } - const fd = fs.openSync(paths.events, 'r+'); + const fd = openVerifiedCanonicalTailRepairFd(); try { fs.ftruncateSync(fd, Buffer.byteLength(prefixContent, 'utf8')); fs.fsyncSync(fd); + verifyCanonicalTailRepairFd(fd); } finally { fs.closeSync(fd); } From 8a808475bea114b9bb4ea010b38e4498b9395ae3 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 09:37:23 +0200 Subject: [PATCH 119/177] docs(t04): record H57 red/green CI and review-thread dispositions --- memory.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/memory.md b/memory.md index 2ff59ab99..5c463e228 100644 --- a/memory.md +++ b/memory.md @@ -12,6 +12,10 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-H57 canonical ledger tail-repair descriptor race (31 October 2026; NOT ACCEPTED):** After layout/link preflight, a swapped hard link at the `events.jsonl` `r+` open allowed newline completion or torn-tail truncation to mutate an unrelated file. Added two fail-first `T04-H57` hard-link swap-at-open regressions directly on authoritative draft PR #92 in `2c8f9cc024768213d6d66ce650623daeead7336c`; **both Ubuntu and Windows** reproduced the unsafe repair in [RED CI #41180716611](https://github.com/eybersjp/development-kit/actions/runs/41180716611). Narrow runtime fix `4e969e2578b8c42ddc05c06ed304fae55baed78e` binds repair to the initial canonical inode, opens with descriptor no-follow where supported, requires single-link regular-file identity and unchanged descriptor bytes before any append/truncate, and rechecks canonical identity after fsync. **Both full Ubuntu and Windows validation jobs PASS** at this exact code head in [GREEN CI #41180784890](https://github.com/eybersjp/development-kit/actions/runs/41180784890); the independent risk-3 acceptance job correctly FAILS for missing exact-source independent reviews. Engineering replied with code/test/CI evidence and dispositioned **11 of 13** PR #92 review threads as fixed (not independent approvals); the two remaining OPEN findings concern genuine NTFS 8.3 ancestor journal alias and staged destination collision tests. Issue #73 controlled physical NTFS power-loss evidence, three distinct independent exact-source code/architecture/security approvals, two full validations on the final reviewed SHA, and persisted authentic `ACCEPTED` are still missing. This memory commit itself changes PR #92 HEAD; run fresh final-head CI. No merge, release, fabricated approval or physical-test claim. + + + - **T04-SEC-JSON-01 lossless canonical JSON boundary (31 October 2026; NOT ACCEPTED):** `cloneSerializable()` previously accepted Map, Set, Date, undefined object values and non-finite numbers because successful `JSON.stringify()` was mistaken for lossless serializability. An acknowledged V2 event could therefore silently discard or transform state data. Isolated **draft, DO NOT MERGE** diagnostic [PR #132](https://github.com/eybersjp/development-kit/pull/132) introduced the negative-control test first at `3ae982c72fbee77bf38768f813e80547cf36a623`; both Ubuntu and Windows `Validate v0.13 State Engine V2 increment` failed as expected in [RED CI #41176319558](https://github.com/eybersjp/development-kit/actions/runs/41176319558) (first failure: Map was acknowledged without throwing). Narrow correction uses Node `isDeepStrictEqual` against the canonical JSON parse round-trip and rejects lossy state before ledger publication. Diagnostic SHA `728291c317eb7b009a1c8164437b7869b279052f` passed **both complete Ubuntu and Windows v0.13 validation jobs** in [GREEN CI #41176399858](https://github.com/eybersjp/development-kit/actions/runs/41176399858). Ported only the validated regression and runtime change to authoritative draft PR #92 in `e11da547b776c2cf306974535d1fd20f5ed6f7bb` and `5d39940177330274ba6e37033e1c316d08263487`. This memory update changes the exact integration SHA; fresh cross-platform validation and independent exact-source review are required. **Still NOT ACCEPTED:** 13 PR #92 review threads, distinct code/architecture/security approvals, controlled physical NTFS power-loss Issue #73 evidence, two complete identical-final-SHA CI executions and persisted authentic verdict. No merges, waivers or v0.12 T04 inclusion. From 9cd23fe9039b146fbfeacb63b86c1881cc7519a6 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 09:46:50 +0200 Subject: [PATCH 120/177] test(t04): reproduce unreadable tail witness and torn UTF-8 repair regressions --- scripts/state-engine-v2-hardening.test.mjs | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index bcf9ee139..2a241a3cf 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -1203,3 +1203,53 @@ for (const repairMode of ['missing-terminal-newline', 'torn-trailing-json']) { 'Rejected repair must not alter the substituted canonical path'); }); } + +test('T04-H58 unreadable snapshot witness cannot authorize no-journal torn-tail truncation', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + appendMetadataEvent({ rootDir, eventType: 'ACKNOWLEDGED_EVENT', payload: { value: 2 } }); + const paths = getStateEnginePaths(rootDir); + const ledger = fs.readFileSync(paths.events); + const finalLineStart = ledger.lastIndexOf(0x0a, ledger.length - 2) + 1; + const torn = Buffer.concat([ledger.subarray(0, finalLineStart), + ledger.subarray(finalLineStart, finalLineStart + 21)]); + fs.writeFileSync(paths.events, torn); + assert.equal(fs.existsSync(paths.pending), false); + const originalRead = fs.readFileSync; + let denied = 0; + fs.readFileSync = function denyTailSnapshotWitness(target, ...args) { + if (String(target) === paths.snapshot) { + denied += 1; + const error = new Error('Synthetic snapshot witness access denied'); + error.code = 'EACCES'; + throw error; + } + return originalRead.call(this, target, ...args); + }; + try { + assert.throws(() => loadCanonicalEvents(rootDir), + /snapshot witness|EACCES|access denied/i, + 'An unreadable acknowledged witness must block destructive tail repair'); + } finally { + fs.readFileSync = originalRead; + } + assert.ok(denied > 0, 'Fault injection must intercept the snapshot witness'); + assert.deepEqual(fs.readFileSync(paths.events), torn, + 'Rejected tail repair must retain all original torn canonical bytes'); +}); + +test('T04-H59 incomplete multibyte UTF-8 tail is truncated using raw canonical bytes', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'BASELINE_EVENT', payload: { value: 1 } }); + const paths = getStateEnginePaths(rootDir); + const baseline = fs.readFileSync(paths.events); + const torn = Buffer.concat([baseline, Buffer.from('{"payload":"'), + Buffer.from([0xe2, 0x82])]); + fs.writeFileSync(paths.events, torn); + assert.equal(fs.existsSync(paths.pending), false); + const recovered = loadCanonicalEvents(rootDir); + assert.equal(recovered.length, 1); + assert.deepEqual(fs.readFileSync(paths.events), baseline, + 'A torn UTF-8 suffix must be removed without rewriting valid history'); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From cd066b12166839396b7303ee3f1e1ffee377b541 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 09:47:57 +0200 Subject: [PATCH 121/177] fix(t04): fail closed on unreadable tail witness and compare raw ledger bytes --- runtime/orchestration/state-engine-v2.mjs | 27 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 0b79139ff..4c55cf4e3 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -808,14 +808,26 @@ function parseLedger(content) { } function acknowledgedSnapshotWitness(paths) { - if (!fs.existsSync(paths.snapshot)) return null; + let raw; try { - const snapshot = JSON.parse(fs.readFileSync(paths.snapshot, 'utf8')); + raw = fs.readFileSync(paths.snapshot, 'utf8'); + } catch (error) { + if (error.code === 'ENOENT') return null; + // Unreadable materialized evidence may witness an acknowledged later + // event. Never treat permission/I/O failure as an absent disposable view + // and truncate the canonical ledger without inspecting that witness. + throw new StateEngineError( + 'Cannot inspect materialized snapshot witness before ledger tail repair: ' + + (error.code || error.message), + ); + } + try { + const snapshot = JSON.parse(raw); validateStateSnapshot(snapshot); return snapshot; } catch { - // A corrupt disposable/materialized snapshot cannot authorize destructive - // canonical-ledger repair. It will be rebuilt after canonical recovery. + // Malformed derived JSON/schema is disposable; filesystem read errors + // above are not and must retain the original torn canonical evidence. return null; } } @@ -834,8 +846,11 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { if (!initialEntry.isFile() || initialEntry.isSymbolicLink() || initialEntry.nlink !== 1n) { throw new StateEngineError('Canonical ledger tail repair requires a single-link regular file'); } - const content = fs.readFileSync(paths.events, 'utf8'); - const expectedBytes = Buffer.from(content, 'utf8'); + // Preserve the exact on-disk bytes. Decoding an incomplete multibyte + // trailing character inserts U+FFFD; re-encoding that string must never be + // used as the descriptor witness or to decide that raw bytes changed. + const expectedBytes = fs.readFileSync(paths.events); + const content = expectedBytes.toString('utf8'); function verifyCanonicalTailRepairFd(fd) { const opened = fs.fstatSync(fd, { bigint: true }); From b0716b403448697435e8c81585395d519051b6d0 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 10:02:51 +0200 Subject: [PATCH 122/177] test(t04): port validated NTFS 8.3 restore alias regression fixtures --- scripts/state-engine-v013.test.mjs | 97 ++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/scripts/state-engine-v013.test.mjs b/scripts/state-engine-v013.test.mjs index 37ae4cca1..21eef07a5 100644 --- a/scripts/state-engine-v013.test.mjs +++ b/scripts/state-engine-v013.test.mjs @@ -893,3 +893,100 @@ test('V13-API-PROJECTION-03 V2 metadata alone does not hide valid unmigrated leg await service.stop(); } }); + + +/** + * W73-8DOT3: Use the actual Windows GetShortPathNameW API. These fixtures + * skip only when the test volume does not generate 8.3 aliases; a synthetic + * string-only alias would not prove NTFS directory identity. + */ +function getExistingNtfsShortPath(directory) { + const script = [ + "$ErrorActionPreference='Stop'", + "$code='using System; using System.Text; using System.Runtime.InteropServices; public static class DkfShortPath { [DllImport(\"kernel32.dll\", CharSet=CharSet.Unicode, SetLastError=true)] public static extern uint GetShortPathName(string input, StringBuilder output, uint length); }'", + 'Add-Type -TypeDefinition $code', + '$buffer = New-Object System.Text.StringBuilder(32768)', + '$size = [DkfShortPath]::GetShortPathName($env:DKF_TEST_SHORT_PATH, $buffer, [uint32]$buffer.Capacity)', + 'if ($size -eq 0 -or $size -ge $buffer.Capacity) { exit 2 }', + '[Console]::Out.Write($buffer.ToString())', + ].join('; '); + const result = spawnSync('powershell.exe', + ['-NoProfile', '-NonInteractive', '-Command', script], + { encoding: 'utf8', windowsHide: true, shell: false, timeout: 20000, + env: { ...process.env, DKF_TEST_SHORT_PATH: directory } }); + return result.status === 0 ? result.stdout.trim() : null; +} + +test('W73-8DOT3-01 NTFS ancestor short alias cannot bypass a retained transaction journal', (t) => { + if (process.platform !== 'win32') { t.skip('Requires NTFS Windows'); return; } + const { backupPath } = createLegacyBackupFixture(t); + const container = tempDir(t, 'dk-v013-83-journal-'); + const parent = path.join(container, 'LongDirectoryNameForAlias'); + fs.mkdirSync(parent); + const shortParent = getExistingNtfsShortPath(parent); + if (!shortParent || !path.basename(shortParent).includes('~')) { + t.skip('NTFS 8.3 names are not enabled on this volume'); return; + } + const targetRoot = path.join(parent, 'project'); + const aliasRoot = path.join(shortParent, 'project'); + fs.mkdirSync(targetRoot); + fs.writeFileSync(path.join(targetRoot, 'original.txt'), 'ORIGINAL-83-WITNESS'); + const restored = restoreLegacyBackup({ + backupPath, targetRoot, overwrite: true, confirmOffline: true, + }); + const journalBefore = fs.readFileSync(restored.transactionLog); + const displacedPublication = path.join(parent, 'lost-publication'); + fs.renameSync(targetRoot, displacedPublication); + assert.throws(() => restoreLegacyBackup({ + backupPath, targetRoot: aliasRoot, overwrite: true, confirmOffline: true, + }), /restore.*(alias|witness|transaction|incomplete)|terminal.*root/i); + assert.deepEqual(fs.readFileSync(restored.transactionLog), journalBefore, + 'Short-name retry must not append or rewrite the existing hash-chained journal'); + assert.equal(fs.existsSync(targetRoot), false); + assert.equal(fs.readFileSync(path.join(restored.displacedOriginal, 'original.txt'), 'utf8'), + 'ORIGINAL-83-WITNESS'); +}); + +test('W73-8DOT3-02 signed long/short destination aliases cannot overwrite staged backup bytes', (t) => { + if (process.platform !== 'win32') { t.skip('Requires NTFS Windows'); return; } + const container = tempDir(t, 'dk-v013-83-stage-'); + const probe = path.join(container, 'LongDirectoryNameForAlias'); + fs.mkdirSync(probe); + const shortProbe = getExistingNtfsShortPath(probe); + if (!shortProbe || !path.basename(shortProbe).includes('~')) { + t.skip('NTFS 8.3 names are not enabled on this volume'); return; + } + const shortLeaf = path.basename(shortProbe); + fs.rmdirSync(probe); + const makeEntry = (relative, content) => { + const bytes = Buffer.from(content); + return { + path: relative, bytes: bytes.length, contentBase64: bytes.toString('base64'), + fingerprint: 'sha256:' + crypto.createHash('sha256').update(bytes).digest('hex'), + }; + }; + const files = [ + makeEntry('LongDirectoryNameForAlias/file.json', 'FIRST-SIGNED-BYTES'), + makeEntry(shortLeaf + '/file.json', 'SECOND-SIGNED-BYTES'), + ]; + const manifest = files.map(({ path: name, bytes, fingerprint }) => ({ + bytes, fingerprint, path: name, + })); + const bundle = { + schemaVersion: '1.0.0', fileCount: files.length, files, + sourceFingerprint: 'sha256:' + + crypto.createHash('sha256').update(JSON.stringify(manifest)).digest('hex'), + }; + const backupPath = path.join(container, 'signed-backup.json'); + fs.writeFileSync(backupPath, JSON.stringify(bundle)); + const targetRoot = path.join(container, 'restored'); + assert.throws(() => restoreLegacyBackup({ + backupPath, targetRoot, confirmOffline: true, + }), /Duplicate physical restore destination alias/i, + 'A second signed entry must not replace the same physical staged inode'); + assert.equal(fs.existsSync(targetRoot), false, + 'Rejected alias collision must not publish a candidate root'); + assert.deepEqual(fs.readdirSync(container).filter(name => + name.startsWith('.dk-legacy-restore-stage-')), [], + 'Rejected prepublication stage must be removed without touching any original root'); +}); From 35b1667d3e7c7dd9956c045aef18bac991fee082 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 10:03:01 +0200 Subject: [PATCH 123/177] docs(t04): record verified NTFS 8.3 test evidence and acceptance boundaries --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index 5c463e228..ab8182214 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **W73-8DOT3 physical NTFS short-name alias regressions (31 October 2026; NOT ACCEPTED):** Source-bound test-only diagnostic draft [PR #134](https://github.com/eybersjp/development-kit/pull/134) forked authoritative PR #92 at `cd066b12166839396b7303ee3f1e1ffee377b541` and added two Windows `GetShortPathNameW` adversarial fixtures: (1) an 8.3-spelled ancestor cannot bypass an existing hash-chained restore journal after simulated lost publication; (2) two signed backup paths with long/short aliases cannot overwrite the same physical staged inode. Diagnostic test commit `81c395e166a9664dd3d1531916ba926b683a6816` passed **both full Ubuntu and Windows v0.13 validation jobs** in [CI #41182432970](https://github.com/eybersjp/development-kit/actions/runs/41182432970); Windows job explicitly reports both `W73-8DOT3-01` and `W73-8DOT3-02` **PASS**, not SKIP (the test would skip when the host has no 8.3 aliases). Only the validated test fixtures were ported to authoritative PR #92 at `b0716b403448697435e8c81585395d519051b6d0`; no runtime behavior or acceptance gate was changed. This evidence exercises physical NTFS alias identity on hosted Windows, **not** sudden physical power-loss durability. The remaining review threads require authenticated disposition; Issue #73 physical power-loss evidence, independent exact-source code/architecture/security approvals, two complete final-reviewed-SHA validations and persisted deterministic `ACCEPTED` remain outstanding. PR #92 stays draft/unmerged and diagnostics stay unmerged. + - **T04-H57 canonical ledger tail-repair descriptor race (31 October 2026; NOT ACCEPTED):** After layout/link preflight, a swapped hard link at the `events.jsonl` `r+` open allowed newline completion or torn-tail truncation to mutate an unrelated file. Added two fail-first `T04-H57` hard-link swap-at-open regressions directly on authoritative draft PR #92 in `2c8f9cc024768213d6d66ce650623daeead7336c`; **both Ubuntu and Windows** reproduced the unsafe repair in [RED CI #41180716611](https://github.com/eybersjp/development-kit/actions/runs/41180716611). Narrow runtime fix `4e969e2578b8c42ddc05c06ed304fae55baed78e` binds repair to the initial canonical inode, opens with descriptor no-follow where supported, requires single-link regular-file identity and unchanged descriptor bytes before any append/truncate, and rechecks canonical identity after fsync. **Both full Ubuntu and Windows validation jobs PASS** at this exact code head in [GREEN CI #41180784890](https://github.com/eybersjp/development-kit/actions/runs/41180784890); the independent risk-3 acceptance job correctly FAILS for missing exact-source independent reviews. Engineering replied with code/test/CI evidence and dispositioned **11 of 13** PR #92 review threads as fixed (not independent approvals); the two remaining OPEN findings concern genuine NTFS 8.3 ancestor journal alias and staged destination collision tests. Issue #73 controlled physical NTFS power-loss evidence, three distinct independent exact-source code/architecture/security approvals, two full validations on the final reviewed SHA, and persisted authentic `ACCEPTED` are still missing. This memory commit itself changes PR #92 HEAD; run fresh final-head CI. No merge, release, fabricated approval or physical-test claim. From 017916a4662bd7f271e92e4548415d09aa93dd79 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 10:51:35 +0200 Subject: [PATCH 124/177] fix(t04): bind security-control receipts to verified full CI runs --- memory.md | 2 ++ scripts/v013-t04-acceptance-ci-contract.test.mjs | 7 +++++++ scripts/v013-t04-acceptance-gate.mjs | 2 ++ 3 files changed, 11 insertions(+) diff --git a/memory.md b/memory.md index ab8182214..daa84d30a 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-A22 authenticated security-control CI provenance (31 October 2026; NOT ACCEPTED):** Diagnostic draft [PR #136](https://github.com/eybersjp/development-kit/pull/136) branched from authoritative PR #92 source `35b1667d3e7c7dd9956c045aef18bac991fee082`. Test-only SHA `efd80c2068f180a1bed5337d7804887efce03e94` reproduced the risk-3 false-acceptance gap: an unrelated green same-SHA workflow could be cited as a security-control witness (Ubuntu [CI #41185875905](https://github.com/eybersjp/development-kit/actions/runs/41185875905), `T04-A22` RED). Narrow source correction at `08734c3148d4a8cd2b4f15fc66c47683732620c8` requires every control witness workflowRunId to belong to the already authenticated two full Ubuntu/Windows validation run IDs; both complete platform jobs PASS in [CI #41185891626](https://github.com/eybersjp/development-kit/actions/runs/41185891626). These test/source corrections are intended for authoritative PR #92, not diagnostic merge. Independent reviews, physical NTFS power-loss evidence and canonical ACCEPTED remain outstanding. + - **W73-8DOT3 physical NTFS short-name alias regressions (31 October 2026; NOT ACCEPTED):** Source-bound test-only diagnostic draft [PR #134](https://github.com/eybersjp/development-kit/pull/134) forked authoritative PR #92 at `cd066b12166839396b7303ee3f1e1ffee377b541` and added two Windows `GetShortPathNameW` adversarial fixtures: (1) an 8.3-spelled ancestor cannot bypass an existing hash-chained restore journal after simulated lost publication; (2) two signed backup paths with long/short aliases cannot overwrite the same physical staged inode. Diagnostic test commit `81c395e166a9664dd3d1531916ba926b683a6816` passed **both full Ubuntu and Windows v0.13 validation jobs** in [CI #41182432970](https://github.com/eybersjp/development-kit/actions/runs/41182432970); Windows job explicitly reports both `W73-8DOT3-01` and `W73-8DOT3-02` **PASS**, not SKIP (the test would skip when the host has no 8.3 aliases). Only the validated test fixtures were ported to authoritative PR #92 at `b0716b403448697435e8c81585395d519051b6d0`; no runtime behavior or acceptance gate was changed. This evidence exercises physical NTFS alias identity on hosted Windows, **not** sudden physical power-loss durability. The remaining review threads require authenticated disposition; Issue #73 physical power-loss evidence, independent exact-source code/architecture/security approvals, two complete final-reviewed-SHA validations and persisted deterministic `ACCEPTED` remain outstanding. PR #92 stays draft/unmerged and diagnostics stay unmerged. diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs index 1253e19c5..e1e534132 100644 --- a/scripts/v013-t04-acceptance-ci-contract.test.mjs +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -143,3 +143,10 @@ test('T04-A21 ACCEPTED verdict must be uploaded durably, only after a successful 'The canonical verdict lives in a hidden .development-kit directory and must be included'); assert.doesNotMatch(upload, /if-no-files-found: ignore/); }); + + +test('T04-A22 a security-control witness cannot cite an unrelated green workflow', () => { + const gate = fs.readFileSync(path.join(root, 'scripts/v013-t04-acceptance-gate.mjs'), 'utf8'); + assert.match(gate, /validationRuns\.has\(witness\.workflowRunId\)/, + 'Each security-control witness must cite one of the authenticated, exact-SHA, full Ubuntu+Windows validation runs, not any unrelated green workflow'); +}); diff --git a/scripts/v013-t04-acceptance-gate.mjs b/scripts/v013-t04-acceptance-gate.mjs index a1c510fff..81c43314d 100644 --- a/scripts/v013-t04-acceptance-gate.mjs +++ b/scripts/v013-t04-acceptance-gate.mjs @@ -276,6 +276,8 @@ for (const control of requiredControls) { Number.isSafeInteger(witness.githubReviewId) && Number.isSafeInteger(witness.workflowRunId), 'Security control missing independent source-bound witness: ' + control.id); + assert.ok(validationRuns.has(witness.workflowRunId), + 'Security control witness must cite an authenticated full Ubuntu/Windows validation run on the exact reviewed source'); assert.equal(witness.githubReviewId, authenticatedReviews.get('security-reviewer').id, 'Security control witnesses must bind the independently approved security review'); const witnessReview = await api('pulls/92/reviews/' + witness.githubReviewId); From cfdf79847bbc948e7c05ed95ee6bcebc6c11a007 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 12:06:34 +0200 Subject: [PATCH 125/177] fix(t04): source-bind effective physical NTFS approval (A23 red/green CI) --- memory.md | 2 ++ .../v013-t04-acceptance-ci-contract.test.mjs | 21 +++++++++++++++++++ scripts/v013-t04-acceptance-gate.mjs | 3 +++ 3 files changed, 26 insertions(+) diff --git a/memory.md b/memory.md index daa84d30a..455850204 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-A23 physical NTFS reviewer revocation provenance (31 October 2026; NOT ACCEPTED):** On actual PR #92 source `017916a4662bd7f271e92e4548415d09aa93dd79`, the physical power-loss reviewer was checked only by a historical `APPROVED` receipt; a later `CHANGES_REQUESTED` or different-source reapproval could silently supersede it. Isolated draft diagnostic [PR #138](https://github.com/eybersjp/development-kit/pull/138) added test-first `T04-A23` in `813811dd68ef7ee5b7eb0782dd204da0aa2d20d8`; both Ubuntu and Windows reproduced RED at the required negative-control step in [CI #41190631145](https://github.com/eybersjp/development-kit/actions/runs/41190631145). Narrow fix `5718b62fb3219080c2b367c01634063927b0e199` applies the already tested authenticated latest-decisive-review guard to the separate physical NTFS approval; **both complete Ubuntu and Windows validation jobs PASS** in [CI #41190734296](https://github.com/eybersjp/development-kit/actions/runs/41190734296), diagnostic acceptance skipped. Only this tested gate change, its regression and this memory entry are ported to the authoritative draft PR #92. The physical NTFS power-loss test itself, Issue #73 closure, three distinct independent code/architecture/security approvals, two final-head full validation runs and authentic persisted `ACCEPTED` are **still missing**. No diagnostic or integration merge/release is authorized. + - **T04-A22 authenticated security-control CI provenance (31 October 2026; NOT ACCEPTED):** Diagnostic draft [PR #136](https://github.com/eybersjp/development-kit/pull/136) branched from authoritative PR #92 source `35b1667d3e7c7dd9956c045aef18bac991fee082`. Test-only SHA `efd80c2068f180a1bed5337d7804887efce03e94` reproduced the risk-3 false-acceptance gap: an unrelated green same-SHA workflow could be cited as a security-control witness (Ubuntu [CI #41185875905](https://github.com/eybersjp/development-kit/actions/runs/41185875905), `T04-A22` RED). Narrow source correction at `08734c3148d4a8cd2b4f15fc66c47683732620c8` requires every control witness workflowRunId to belong to the already authenticated two full Ubuntu/Windows validation run IDs; both complete platform jobs PASS in [CI #41185891626](https://github.com/eybersjp/development-kit/actions/runs/41185891626). These test/source corrections are intended for authoritative PR #92, not diagnostic merge. Independent reviews, physical NTFS power-loss evidence and canonical ACCEPTED remain outstanding. - **W73-8DOT3 physical NTFS short-name alias regressions (31 October 2026; NOT ACCEPTED):** Source-bound test-only diagnostic draft [PR #134](https://github.com/eybersjp/development-kit/pull/134) forked authoritative PR #92 at `cd066b12166839396b7303ee3f1e1ffee377b541` and added two Windows `GetShortPathNameW` adversarial fixtures: (1) an 8.3-spelled ancestor cannot bypass an existing hash-chained restore journal after simulated lost publication; (2) two signed backup paths with long/short aliases cannot overwrite the same physical staged inode. Diagnostic test commit `81c395e166a9664dd3d1531916ba926b683a6816` passed **both full Ubuntu and Windows v0.13 validation jobs** in [CI #41182432970](https://github.com/eybersjp/development-kit/actions/runs/41182432970); Windows job explicitly reports both `W73-8DOT3-01` and `W73-8DOT3-02` **PASS**, not SKIP (the test would skip when the host has no 8.3 aliases). Only the validated test fixtures were ported to authoritative PR #92 at `b0716b403448697435e8c81585395d519051b6d0`; no runtime behavior or acceptance gate was changed. This evidence exercises physical NTFS alias identity on hosted Windows, **not** sudden physical power-loss durability. The remaining review threads require authenticated disposition; Issue #73 physical power-loss evidence, independent exact-source code/architecture/security approvals, two complete final-reviewed-SHA validations and persisted deterministic `ACCEPTED` remain outstanding. PR #92 stays draft/unmerged and diagnostics stay unmerged. diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs index e1e534132..28ad210f9 100644 --- a/scripts/v013-t04-acceptance-ci-contract.test.mjs +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -150,3 +150,24 @@ test('T04-A22 a security-control witness cannot cite an unrelated green workflow assert.match(gate, /validationRuns\.has\(witness\.workflowRunId\)/, 'Each security-control witness must cite one of the authenticated, exact-SHA, full Ubuntu+Windows validation runs, not any unrelated green workflow'); }); + + +test('T04-A23 physical NTFS power-loss approval must remain effective after later reviews', async () => { + const gate = fs.readFileSync(path.join(root, 'scripts/v013-t04-acceptance-gate.mjs'), 'utf8'); + assert.match(gate, /assertCurrentRoleApproval\(physicalReview, allSubmittedReviews, reviewedCommit\)/, + 'Physical NTFS approval must be checked against the complete authenticated review timeline; an earlier APPROVED receipt can be revoked'); + + const { assertCurrentRoleApproval } = await import('./v013-t04-review-provenance.mjs'); + const sha = 'a'.repeat(40); + const approval = { id: 901, state: 'APPROVED', commit_id: sha, + user: { login: 'independent-ntfs-reviewer' }, submitted_at: '2026-10-31T08:00:00Z' }; + const revoked = { ...approval, id: 902, state: 'CHANGES_REQUESTED', + submitted_at: '2026-10-31T08:30:00Z' }; + const staleReapproval = { ...approval, id: 903, commit_id: 'b'.repeat(40), + submitted_at: '2026-10-31T09:00:00Z' }; + assert.doesNotThrow(() => assertCurrentRoleApproval(approval, [approval], sha)); + assert.throws(() => assertCurrentRoleApproval(approval, [approval, revoked], sha), + /superseded|revoked|effective/i); + assert.throws(() => assertCurrentRoleApproval(approval, [approval, staleReapproval], sha), + /superseded|stale|effective/i); +}); diff --git a/scripts/v013-t04-acceptance-gate.mjs b/scripts/v013-t04-acceptance-gate.mjs index 81c43314d..5a8a58ba1 100644 --- a/scripts/v013-t04-acceptance-gate.mjs +++ b/scripts/v013-t04-acceptance-gate.mjs @@ -163,6 +163,9 @@ assert.equal(physicalReview.state, 'APPROVED'); assert.ok(physicalReview.user?.login && physicalReview.user.login !== pr.user.login, 'Physical NTFS power-loss reviewer must be independent of the source author'); assert.equal(physicalReview.commit_id, reviewedCommit); +// A previously approved physical test may later be revoked or superseded. +// Apply the same authenticated latest-decisive-review guard as the three roles. +assertCurrentRoleApproval(physicalReview, allSubmittedReviews, reviewedCommit); assert.ok((physicalReview.body || '').includes('DKF130-T04 NTFS-PHYSICAL-POWER-LOSS ' + reviewedCommit), 'Independent physical test review must explicitly bind source and evidence'); const artifactRun = Number(physical.artifactUrl.match(/\/actions\/runs\/([0-9]+)/)?.[1]); From 8bf8c0eebb0db6b75752556d4b0e4d2cb8d02d20 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 12:50:59 +0200 Subject: [PATCH 126/177] test(t04): RED A24 reject renamed GitHub identity review revocation --- .../v013-t04-acceptance-ci-contract.test.mjs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs index 28ad210f9..3024220f1 100644 --- a/scripts/v013-t04-acceptance-ci-contract.test.mjs +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -171,3 +171,25 @@ test('T04-A23 physical NTFS power-loss approval must remain effective after late assert.throws(() => assertCurrentRoleApproval(approval, [approval, staleReapproval], sha), /superseded|stale|effective/i); }); + +test('T04-A24 immutable reviewer identity prevents account-rename revocation bypass', async () => { + const { assertCurrentRoleApproval } = await import('./v013-t04-review-provenance.mjs'); + const gate = fs.readFileSync(path.join(root, 'scripts/v013-t04-acceptance-gate.mjs'), 'utf8'); + assert.match(gate, /review\.user\.id/); + assert.match(gate, /pr\.user\.id/); + assert.match(gate, /physicalReview\.user\.id/); + const sha = 'a'.repeat(40); + const prior = { id: 2401, state: 'APPROVED', commit_id: sha, + user: { id: 90321, login: 'reviewer-before-rename' }, + submitted_at: '2026-10-31T08:00:00Z' }; + const revoked = { ...prior, id: 2402, state: 'CHANGES_REQUESTED', + user: { id: 90321, login: 'reviewer-after-rename' }, + submitted_at: '2026-10-31T09:00:00Z' }; + const stale = { ...prior, id: 2403, commit_id: 'b'.repeat(40), + user: { id: 90321, login: 'reviewer-after-rename' }, + submitted_at: '2026-10-31T10:00:00Z' }; + assert.throws(() => assertCurrentRoleApproval(prior, [prior, revoked], sha), + /superseded|revoked|effective/i); + assert.throws(() => assertCurrentRoleApproval(prior, [prior, stale], sha), + /superseded|stale|effective/i); +}); From bbf54a07b6e7d8d31334eb833f66dec0cfa2d6f2 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 12:52:02 +0200 Subject: [PATCH 127/177] fix(t04): bind review revocation and role independence to immutable GitHub account IDs --- scripts/v013-t04-acceptance-ci-contract.test.mjs | 4 ++-- scripts/v013-t04-acceptance-gate.mjs | 14 +++++++++----- scripts/v013-t04-review-provenance.mjs | 10 ++++++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs index 3024220f1..14fef90df 100644 --- a/scripts/v013-t04-acceptance-ci-contract.test.mjs +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -103,7 +103,7 @@ test('T04-A19 later review revocation or stale reapproval cannot be hidden by an const sha = 'a'.repeat(40); const other = 'b'.repeat(40); const approval = { id: 101, state: 'APPROVED', commit_id: sha, - user: { login: 'independent-code-reviewer' }, submitted_at: '2026-10-30T10:00:00Z' }; + user: { id: 90322, login: 'independent-code-reviewer' }, submitted_at: '2026-10-30T10:00:00Z' }; const comment = { ...approval, id: 102, state: 'COMMENTED', submitted_at: '2026-10-30T10:30:00Z' }; const revoked = { ...approval, id: 103, state: 'CHANGES_REQUESTED', @@ -160,7 +160,7 @@ test('T04-A23 physical NTFS power-loss approval must remain effective after late const { assertCurrentRoleApproval } = await import('./v013-t04-review-provenance.mjs'); const sha = 'a'.repeat(40); const approval = { id: 901, state: 'APPROVED', commit_id: sha, - user: { login: 'independent-ntfs-reviewer' }, submitted_at: '2026-10-31T08:00:00Z' }; + user: { id: 90323, login: 'independent-ntfs-reviewer' }, submitted_at: '2026-10-31T08:00:00Z' }; const revoked = { ...approval, id: 902, state: 'CHANGES_REQUESTED', submitted_at: '2026-10-31T08:30:00Z' }; const staleReapproval = { ...approval, id: 903, commit_id: 'b'.repeat(40), diff --git a/scripts/v013-t04-acceptance-gate.mjs b/scripts/v013-t04-acceptance-gate.mjs index 5a8a58ba1..bafc8edbb 100644 --- a/scripts/v013-t04-acceptance-gate.mjs +++ b/scripts/v013-t04-acceptance-gate.mjs @@ -85,6 +85,8 @@ assert.equal(pr.state, 'open'); assert.equal(pr.head.sha, event.pull_request.head.sha, 'Stale PR event/source'); assert.equal(pr.base.ref, 'main'); assert.equal(pr.head.repo.full_name, REPO); +assert.ok(Number.isSafeInteger(pr.user?.id) && pr.user.id > 0, + 'Immutable authenticated PR author ID required'); // Distinct GitHub-authored, post-source reviews. Self-authored PASS strings are // never an approval. Each review must explicitly identify its role and SHA. @@ -111,10 +113,11 @@ for (const role of ROLES) { const review = await api('pulls/92/reviews/' + receipt.githubReviewId); assert.equal(review.commit_id, reviewedCommit, 'Review is stale'); assert.equal(review.state, 'APPROVED', 'Only authenticated GitHub approval is PASS'); - assert.ok(review.user?.login && review.user.login !== pr.user.login, - 'Author cannot independently approve their own code'); - assert.ok(!reviewers.has(review.user.login), 'Distinct reviewer identity required for each role'); - reviewers.add(review.user.login); + assert.ok(Number.isSafeInteger(review.user?.id) && review.user.id > 0 && + review.user.id !== pr.user.id, + 'Author cannot independently approve their own code, including after account rename'); + assert.ok(!reviewers.has(review.user.id), 'Distinct immutable reviewer ID required for each role'); + reviewers.add(review.user.id); assertCurrentRoleApproval(review, allSubmittedReviews, reviewedCommit); authenticatedReviews.set(role, review); assert.ok((review.body || '').includes('DKF130-T04 ' + role + ' ' + reviewedCommit), @@ -160,7 +163,8 @@ assert.ok(physical && physical.reviewedCommit === reviewedCommit 'Independent, source-bound physical NTFS power-loss evidence is mandatory'); const physicalReview = await api('pulls/92/reviews/' + physical.independentReviewId); assert.equal(physicalReview.state, 'APPROVED'); -assert.ok(physicalReview.user?.login && physicalReview.user.login !== pr.user.login, +assert.ok(Number.isSafeInteger(physicalReview.user?.id) && physicalReview.user.id > 0 && + physicalReview.user.id !== pr.user.id, 'Physical NTFS power-loss reviewer must be independent of the source author'); assert.equal(physicalReview.commit_id, reviewedCommit); // A previously approved physical test may later be revoked or superseded. diff --git a/scripts/v013-t04-review-provenance.mjs b/scripts/v013-t04-review-provenance.mjs index 78dc8f216..c77b8f2e7 100644 --- a/scripts/v013-t04-review-provenance.mjs +++ b/scripts/v013-t04-review-provenance.mjs @@ -9,12 +9,14 @@ export function assertCurrentRoleApproval(approval, submittedReviews, reviewedCo 'Authenticated role review ID is required'); assert.equal(approval.state, 'APPROVED', 'Role review must be approved'); assert.equal(approval.commit_id, reviewedCommit, 'Role approval source is stale'); - const login = approval.user?.login; - assert.ok(typeof login === 'string' && login.length > 0, - 'Role reviewer identity is missing'); + // GitHub logins are mutable and case-insensitive; the numeric account ID is + // stable across renames and must bind every later decisive submission. + const accountId = approval.user?.id; + assert.ok(Number.isSafeInteger(accountId) && accountId > 0, + 'Immutable authenticated GitHub reviewer ID is required'); assert.ok(Array.isArray(submittedReviews), 'Authenticated PR review timeline is unavailable'); const decisive = submittedReviews.filter(review => - review?.user?.login?.toLowerCase() === login.toLowerCase() && + review?.user?.id === accountId && ['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(review.state)); assert.ok(decisive.length > 0, 'Role approval absent from authenticated review timeline'); for (const review of decisive) { From 4cc9400f9c556ecfae14729cfc8617f0bec2bce9 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 13:01:10 +0200 Subject: [PATCH 128/177] docs(t04): record A24 authenticated reviewer identity red-green CI and remaining gates --- docs/03-reference/scripts/v013-t04-review-provenance.md | 4 ++-- memory.md | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/03-reference/scripts/v013-t04-review-provenance.md b/docs/03-reference/scripts/v013-t04-review-provenance.md index eb0b429bd..dc8199455 100644 --- a/docs/03-reference/scripts/v013-t04-review-provenance.md +++ b/docs/03-reference/scripts/v013-t04-review-provenance.md @@ -8,8 +8,8 @@ A valid role receipt identifies a distinct authenticated GitHub `APPROVED` review bound to the immutable reviewed source commit. The single-review endpoint alone is insufficient: an earlier approved review may have been superseded by a later `CHANGES_REQUESTED`, `DISMISSED`, or approval on a different commit. -`assertCurrentRoleApproval(approval, submittedReviews, reviewedCommit)` examines the authenticated, paginated pull-request review timeline for that reviewer. It ignores non-decisive `COMMENTED` submissions, requires valid submitted-at ordering witnesses, and fails closed unless the latest decisive review is the exact approved receipt for the exact reviewed commit. The caller must retrieve the review timeline through authenticated GitHub API calls and retain the independent role, distinct identity, zero unresolved threads, and physical NTFS evidence gates. +`assertCurrentRoleApproval(approval, submittedReviews, reviewedCommit)` examines the authenticated, paginated pull-request review timeline using the immutable positive numeric GitHub `user.id` (not mutable/case-insensitive `user.login`). It ignores non-decisive `COMMENTED` submissions, requires valid submitted-at ordering witnesses, and fails closed unless the latest decisive review by the same account, including after username changes, is the exact approved receipt for the exact reviewed commit. The production gate also binds distinct role reviewers, source-author exclusion and the independent physical NTFS reviewer to authenticated numeric account IDs. The caller must retrieve the review timeline through authenticated GitHub API calls and retain the independent role, distinct identity, zero unresolved threads, and physical NTFS evidence gates. ## Validation -`node --test scripts/v013-t04-acceptance-ci-contract.test.mjs` includes T04-A19 adversarial cases for revoked approvals, stale reapprovals, dismissed reviews, and harmless later comments. Both Ubuntu and Windows full CI jobs must pass on the same candidate commit. This helper never issues approvals, marks review threads resolved, merges PRs, or emits `ACCEPTED` by itself. +`node --test scripts/v013-t04-acceptance-ci-contract.test.mjs` includes T04-A19/A23/A24 adversarial cases for revoked approvals, stale reapprovals, dismissed reviews, and harmless later comments. Both Ubuntu and Windows full CI jobs must pass on the same candidate commit. This helper never issues approvals, marks review threads resolved, merges PRs, or emits `ACCEPTED` by itself. diff --git a/memory.md b/memory.md index 455850204..406d52583 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,9 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-A24 immutable GitHub reviewer identity (31 October 2026; NOT ACCEPTED):** The risk-3 gate and effective-review helper previously identified independent reviewers and their later decisive submissions by mutable GitHub login. An account rename could hide a later `CHANGES_REQUESTED` or stale reapproval and permit a historical approval to remain apparently effective; distinct role/author checks also used mutable login. Isolated unmerged diagnostic [PR #140](https://github.com/eybersjp/development-kit/pull/140) test-only SHA `8bf8c0eebb0db6b75752556d4b0e4d2cb8d02d20` reproduced T04-A24 RED on **both Ubuntu and Windows** in [CI #41194429027](https://github.com/eybersjp/development-kit/actions/runs/41194429027). Corrected diagnostic SHA `bbf54a07b6e7d8d31334eb833f66dec0cfa2d6f2` requires positive immutable GitHub numeric account IDs for reviewer provenance, distinct role identities, source-author exclusion and physical NTFS reviewer exclusion; both full Ubuntu/Windows validation jobs PASS in [CI #41194490767](https://github.com/eybersjp/development-kit/actions/runs/41194490767), with acceptance correctly skipped on diagnostic. The **exact validated source/test commit** was fast-forward ported to authoritative draft PR #92, with no diagnostic merge. This documentation update creates a newer integration head requiring fresh CI and source-bound review. **Still NOT ACCEPTED:** three genuine distinct exact-source role approvals, independently verified physical NTFS sudden-power-loss evidence/Issue #73 resolution, two complete final-head Ubuntu/Windows runs and authentic persisted deterministic `ACCEPTED`. PR #92 remains draft/unmerged and T04 excluded from v0.12. + + - **T04-A23 physical NTFS reviewer revocation provenance (31 October 2026; NOT ACCEPTED):** On actual PR #92 source `017916a4662bd7f271e92e4548415d09aa93dd79`, the physical power-loss reviewer was checked only by a historical `APPROVED` receipt; a later `CHANGES_REQUESTED` or different-source reapproval could silently supersede it. Isolated draft diagnostic [PR #138](https://github.com/eybersjp/development-kit/pull/138) added test-first `T04-A23` in `813811dd68ef7ee5b7eb0782dd204da0aa2d20d8`; both Ubuntu and Windows reproduced RED at the required negative-control step in [CI #41190631145](https://github.com/eybersjp/development-kit/actions/runs/41190631145). Narrow fix `5718b62fb3219080c2b367c01634063927b0e199` applies the already tested authenticated latest-decisive-review guard to the separate physical NTFS approval; **both complete Ubuntu and Windows validation jobs PASS** in [CI #41190734296](https://github.com/eybersjp/development-kit/actions/runs/41190734296), diagnostic acceptance skipped. Only this tested gate change, its regression and this memory entry are ported to the authoritative draft PR #92. The physical NTFS power-loss test itself, Issue #73 closure, three distinct independent code/architecture/security approvals, two final-head full validation runs and authentic persisted `ACCEPTED` are **still missing**. No diagnostic or integration merge/release is authorized. - **T04-A22 authenticated security-control CI provenance (31 October 2026; NOT ACCEPTED):** Diagnostic draft [PR #136](https://github.com/eybersjp/development-kit/pull/136) branched from authoritative PR #92 source `35b1667d3e7c7dd9956c045aef18bac991fee082`. Test-only SHA `efd80c2068f180a1bed5337d7804887efce03e94` reproduced the risk-3 false-acceptance gap: an unrelated green same-SHA workflow could be cited as a security-control witness (Ubuntu [CI #41185875905](https://github.com/eybersjp/development-kit/actions/runs/41185875905), `T04-A22` RED). Narrow source correction at `08734c3148d4a8cd2b4f15fc66c47683732620c8` requires every control witness workflowRunId to belong to the already authenticated two full Ubuntu/Windows validation run IDs; both complete platform jobs PASS in [CI #41185891626](https://github.com/eybersjp/development-kit/actions/runs/41185891626). These test/source corrections are intended for authoritative PR #92, not diagnostic merge. Independent reviews, physical NTFS power-loss evidence and canonical ACCEPTED remain outstanding. From 8fb1e51aca3805b8f83935796b7d47cc5d95f266 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 13:12:31 +0200 Subject: [PATCH 129/177] test(T04-H60): fail closed on ENOSPC during valid ledger tail newline repair --- scripts/state-engine-v2-hardening.test.mjs | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index 2a241a3cf..3b248b3e0 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -1253,3 +1253,37 @@ test('T04-H59 incomplete multibyte UTF-8 tail is truncated using raw canonical b 'A torn UTF-8 suffix must be removed without rewriting valid history'); assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); }); + +test('T04-H60 valid unterminated ledger event survives ENOSPC during newline repair', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H60_VALID_EVENT', payload: { value: 60 } }); + const paths = getStateEnginePaths(rootDir); + const validWithoutNewline = fs.readFileSync(paths.events).subarray(0, -1); + fs.writeFileSync(paths.events, validWithoutNewline); + // No acknowledged derived witness: a failed newline write must not be + // misclassified as malformed JSON and authorize destructive truncation. + fs.unlinkSync(paths.snapshot); + const canonical = fs.statSync(paths.events); + const originalWrite = fs.writeSync; + let injected = 0; + fs.writeSync = function denyLedgerNewline(fd, buffer, ...args) { + const stat = fs.fstatSync(fd); + if (stat.isFile() && stat.dev === canonical.dev && stat.ino === canonical.ino && + Buffer.isBuffer(buffer) && buffer.length === 1 && buffer[0] === 10) { + injected += 1; + const error = new Error('Synthetic ENOSPC during valid event newline completion'); + error.code = 'ENOSPC'; + throw error; + } + return originalWrite.call(this, fd, buffer, ...args); + }; + try { + assert.throws(() => loadCanonicalEvents(rootDir), /ENOSPC|newline completion/i, + 'A failed valid-event repair write must fail closed, never truncate history'); + } finally { + fs.writeSync = originalWrite; + } + assert.equal(injected, 1, 'Fault injection must reach the actual canonical repair write'); + assert.deepEqual(fs.readFileSync(paths.events), validWithoutNewline, + 'A failed newline repair must preserve the complete valid event bytes'); +}); From 557bfedd7514cfeec6d249506658f1cd2beab7ff Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 13:15:06 +0200 Subject: [PATCH 130/177] fix(T04-H60): never truncate valid canonical event after newline I/O failure --- runtime/orchestration/state-engine-v2.mjs | 30 +++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 4c55cf4e3..40d529c61 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -898,28 +898,32 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { const prefixEvents = parseLedger(prefixContent); const previous = prefixEvents.at(-1) ?? null; - // A fully valid final event that merely lost its record terminator is - // recoverable without discarding canonical information. + // Only a JSON syntax error can authorize torn-suffix truncation. A + // successfully parsed event must either validate and complete its newline, + // or fail closed on any write, fsync, descriptor or integrity error. + // Never classify ENOSPC/EIO during newline completion as malformed JSON: + // doing so could truncate an otherwise valid canonical event. + let parsedTail; + let completeJson = false; try { - const event = JSON.parse(tail); - validateStateEvent(event, previous); + parsedTail = JSON.parse(tail); + completeJson = true; + } catch (error) { + if (!(error instanceof SyntaxError)) throw error; + } + if (completeJson) { + validateStateEvent(parsedTail, previous); const fd = openVerifiedCanonicalTailRepairFd(); try { const stat = fs.fstatSync(fd); if (!stat.isFile()) throw new StateEngineError('Canonical ledger repair target is not a regular file'); - fs.writeSync(fd, Buffer.from('\n'), 0, 1, Buffer.byteLength(content, 'utf8')); + fs.writeSync(fd, Buffer.from('\\n'), 0, 1, Buffer.byteLength(content, 'utf8')); fs.fsyncSync(fd); - verifyCanonicalTailRepairFd(fd); + verifyCanonicalTailRepairFd(fd); } finally { fs.closeSync(fd); } - return [...prefixEvents, event]; - } catch (error) { - if (error instanceof StateEngineError && !String(error.message).includes('JSON')) { - // A parseable event with an invalid sequence/hash is ambiguous - // corruption, not a torn byte suffix. Never truncate it automatically. - throw error; - } + return [...prefixEvents, parsedTail]; } // The final suffix is not valid JSON. It may be repaired only when no From f5f93e1bb7e350972cd7a8e11892501467724dcf Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 13:15:15 +0200 Subject: [PATCH 131/177] fix(T04-H60): use explicit LF byte in verified tail completion --- runtime/orchestration/state-engine-v2.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 40d529c61..f8a68aef4 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -917,7 +917,7 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { try { const stat = fs.fstatSync(fd); if (!stat.isFile()) throw new StateEngineError('Canonical ledger repair target is not a regular file'); - fs.writeSync(fd, Buffer.from('\\n'), 0, 1, Buffer.byteLength(content, 'utf8')); + fs.writeSync(fd, Buffer.from([0x0a]), 0, 1, Buffer.byteLength(content, 'utf8')); fs.fsyncSync(fd); verifyCanonicalTailRepairFd(fd); } finally { From dfbfd0294b84b09dc265a35d4dc3b63b93b92f0c Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 13:21:26 +0200 Subject: [PATCH 132/177] docs(T04-H60): record red/green cross-platform canonical tail repair evidence --- memory.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/memory.md b/memory.md index 406d52583..fa1fd3ffd 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,9 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-H60 fail-closed valid-event tail repair under ENOSPC (31 October 2026; NOT ACCEPTED):** `repairUnwitnessedTrailingLedgerSuffixLocked()` caught newline write/fsync errors alongside `JSON.parse` errors. A valid, hash-checked event missing only its final LF could be silently truncated if a newline write failed and the snapshot witness was absent. Test-only PR #92 SHA `8fb1e51aca3805b8f83935796b7d47cc5d95f266` reproduced RED on **Ubuntu and Windows** in [CI #41195907240](https://github.com/eybersjp/development-kit/actions/runs/41195907240). Narrow fix separates JSON syntax-error handling from validation and filesystem mutation, preserving valid canonical event bytes and propagating ENOSPC/EIO. Exact code SHA `f5f93e1bb7e350972cd7a8e11892501467724dcf` passed **both complete Ubuntu and Windows** validation jobs in [CI #41196089460](https://github.com/eybersjp/development-kit/actions/runs/41196089460); production acceptance failed closed with `Missing exact-source independent reviews`. Issue #84 contains the test-first evidence. This memory-only commit changes the PR #92 head and requires fresh exact-head CI. Still outstanding: physical NTFS sudden-power-loss Issue #73, genuine distinct code/architecture/security approvals, two final reviewed-source Ubuntu/Windows runs, persisted deterministic `ACCEPTED`, and protected v0.13 release authorization. No merges or waivers. + + - **T04-A24 immutable GitHub reviewer identity (31 October 2026; NOT ACCEPTED):** The risk-3 gate and effective-review helper previously identified independent reviewers and their later decisive submissions by mutable GitHub login. An account rename could hide a later `CHANGES_REQUESTED` or stale reapproval and permit a historical approval to remain apparently effective; distinct role/author checks also used mutable login. Isolated unmerged diagnostic [PR #140](https://github.com/eybersjp/development-kit/pull/140) test-only SHA `8bf8c0eebb0db6b75752556d4b0e4d2cb8d02d20` reproduced T04-A24 RED on **both Ubuntu and Windows** in [CI #41194429027](https://github.com/eybersjp/development-kit/actions/runs/41194429027). Corrected diagnostic SHA `bbf54a07b6e7d8d31334eb833f66dec0cfa2d6f2` requires positive immutable GitHub numeric account IDs for reviewer provenance, distinct role identities, source-author exclusion and physical NTFS reviewer exclusion; both full Ubuntu/Windows validation jobs PASS in [CI #41194490767](https://github.com/eybersjp/development-kit/actions/runs/41194490767), with acceptance correctly skipped on diagnostic. The **exact validated source/test commit** was fast-forward ported to authoritative draft PR #92, with no diagnostic merge. This documentation update creates a newer integration head requiring fresh CI and source-bound review. **Still NOT ACCEPTED:** three genuine distinct exact-source role approvals, independently verified physical NTFS sudden-power-loss evidence/Issue #73 resolution, two complete final-head Ubuntu/Windows runs and authentic persisted deterministic `ACCEPTED`. PR #92 remains draft/unmerged and T04 excluded from v0.12. From 176869e17cd3b9c21b2e5fdb18bd91111b821ecc Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 15:17:26 +0200 Subject: [PATCH 133/177] test(T04-H61): port verified lock initialization EIO regressions from diagnostic PR #141 --- scripts/state-engine-v2-hardening.test.mjs | 87 ++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index 3b248b3e0..e0bd3f9a4 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -1287,3 +1287,90 @@ test('T04-H60 valid unterminated ledger event survives ENOSPC during newline rep assert.deepEqual(fs.readFileSync(paths.events), validWithoutNewline, 'A failed newline repair must preserve the complete valid event bytes'); }); + + +test('T04-H61 failed lock metadata fsync must not strand an orphaned lock', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H61_BASELINE', payload: { step: 1 } }); + const paths = getStateEnginePaths(rootDir); + const before = fs.readFileSync(paths.events); + const originalFsync = fs.fsyncSync; + let injected = false; + fs.fsyncSync = function denyNewLockDurability(fd, ...args) { + const opened = fs.fstatSync(fd, { bigint: true }); + let entry = null; + try { + entry = fs.lstatSync(paths.lock, { bigint: true }); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + if (!injected && entry && opened.isFile() && + opened.dev === entry.dev && opened.ino === entry.ino) { + injected = true; + const error = new Error('injected lock-file fsync failure'); + error.code = 'EIO'; + throw error; + } + return originalFsync.call(this, fd, ...args); + }; + try { + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'H61_REJECTED', payload: { step: 2 } }), + /lock|EIO|durability/i, + ); + } finally { + fs.fsyncSync = originalFsync; + } + assert.equal(injected, true, 'Must exercise failure after exclusive lock-file creation'); + assert.equal(fs.existsSync(paths.lock), false, + 'A failed lock initialization must not strand an unowned lock file'); + assert.deepEqual(fs.readFileSync(paths.events), before, + 'Lock initialization failure must not mutate canonical history'); + appendMetadataEvent({ rootDir, eventType: 'H61_RETRY', payload: { step: 3 } }); + assert.equal(loadCanonicalEvents(rootDir).length, 2); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + + +test('T04-H61 lock metadata write failure must not leave an unowned lock', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H61_WRITE_BASELINE', payload: { step: 1 } }); + const paths = getStateEnginePaths(rootDir); + const before = fs.readFileSync(paths.events); + const originalWriteFile = fs.writeFileSync; + let injected = false; + fs.writeFileSync = function denyLockMetadataWrite(target, ...args) { + if (!injected && typeof target === 'number') { + const opened = fs.fstatSync(target, { bigint: true }); + let lockEntry = null; + try { + lockEntry = fs.lstatSync(paths.lock, { bigint: true }); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + if (lockEntry && opened.isFile() && + opened.dev === lockEntry.dev && opened.ino === lockEntry.ino) { + injected = true; + const error = new Error('injected lock-file write failure'); + error.code = 'EIO'; + throw error; + } + } + return originalWriteFile.call(this, target, ...args); + }; + try { + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'H61_WRITE_REJECTED', payload: { step: 2 } }), + /lock|EIO|write/i, + ); + } finally { + fs.writeFileSync = originalWriteFile; + } + assert.equal(injected, true, 'Must inject after exclusive lock-file creation'); + assert.equal(fs.existsSync(paths.lock), false, + 'Failed lock metadata write must not strand an unowned lock'); + assert.deepEqual(fs.readFileSync(paths.events), before); + appendMetadataEvent({ rootDir, eventType: 'H61_WRITE_RETRY', payload: { step: 3 } }); + assert.equal(loadCanonicalEvents(rootDir).length, 2); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From 21c1ae757d3cfb993024c78261ac092d03e016b1 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 15:17:45 +0200 Subject: [PATCH 134/177] fix(T04-H61): port cross-platform validated fail-closed lock-init cleanup --- runtime/orchestration/state-engine-v2.mjs | 35 ++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index f8a68aef4..c2f4074c4 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -383,7 +383,10 @@ function acquireStateLock(rootDir, timeoutMs = process.platform === 'win32' ? 20 while (Date.now() - started < timeoutMs) { try { const fd = fs.openSync(paths.lock, 'wx'); + let createdIdentity = null; + let initializationFailure = null; try { + createdIdentity = fs.fstatSync(fd, { bigint: true }); fs.writeFileSync(fd, JSON.stringify({ owner, pid: process.pid, @@ -391,8 +394,38 @@ function acquireStateLock(rootDir, timeoutMs = process.platform === 'win32' ? 20 acquiredAt: new Date().toISOString(), }), 'utf8'); fs.fsyncSync(fd); - } finally { + } catch (error) { + initializationFailure = error; + } + try { fs.closeSync(fd); + } catch (error) { + initializationFailure ??= error; + } + if (initializationFailure) { + // The exclusive lock name already exists, but its owner metadata may + // be absent or non-durable. Do not strand an empty/unowned lock after + // ENOSPC, EIO or close failure. Remove only the same single-link inode + // that this process created, never a replacement installed by another + // process. A failed cleanup remains an explicit offline-recovery fault. + try { + const current = fs.lstatSync(paths.lock, { bigint: true }); + if (!createdIdentity || !current.isFile() || current.isSymbolicLink() || + current.nlink !== 1n || current.dev !== createdIdentity.dev || + current.ino !== createdIdentity.ino) { + throw new StateEngineError('Newly created lock identity changed before failed-initialization cleanup'); + } + fs.unlinkSync(paths.lock); + fsyncParentDirectory(paths.stateRoot); + } catch (cleanupError) { + throw new StateEngineError( + 'State Engine lock initialization failed and cleanup was not durably verified; offline recovery required', + { code: 'STATE_LOCK_INIT_CLEANUP_FAILED', + cause: cleanupError.code || cleanupError.message, + initializationCause: initializationFailure.code || initializationFailure.message }, + ); + } + throw initializationFailure; } return { path: paths.lock, owner }; } catch (error) { From b93d8af3230e6173890e6feab430252235a03e80 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 15:17:56 +0200 Subject: [PATCH 135/177] docs(T04-H61): record red-green cross-platform lock recovery evidence and release boundaries --- memory.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/memory.md b/memory.md index fa1fd3ffd..855e5e5a1 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,9 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-H61 lock initialization I/O failure (31 October 2026; NOT ACCEPTED):** A newly created exclusive state-lock file was left behind if its metadata write or fsync failed (EIO/ENOSPC), blocking later writers with an unowned lock. Isolated draft diagnostic [PR #141](https://github.com/eybersjp/development-kit/pull/141) added two fault-injection regressions first: SHA `a641b446cf9de9d93fdf2995d1fb0169ca81964d` reproduced both RED on Ubuntu and Windows in [CI #41204160210](https://github.com/eybersjp/development-kit/actions/runs/41204160210). Narrow fix records the exclusive lock inode, closes its descriptor, removes only the same single-link file after failed initialization, and flushes the parent; unexpected replacement/cleanup failures remain explicit offline-recovery errors. Diagnostic SHA `109a75667b0af1d871e6f7b013bd23f726738919` passed both complete Ubuntu/Windows validation jobs in [CI #41204273231](https://github.com/eybersjp/development-kit/actions/runs/41204273231). Only validated test and source files were ported to authoritative draft PR #92 in commits `176869e17cd3b9c21b2e5fdb18bd91111b821ecc` and `21c1ae757d3cfb993024c78261ac092d03e016b1`. This memory update changes the exact integration SHA, requiring fresh two-run cross-platform CI. No review approval, physical NTFS power-loss proof, ACCEPTED or release is claimed; Issues #71/#73/#84 and protected v0.13 gates remain binding. + + - **T04-H60 fail-closed valid-event tail repair under ENOSPC (31 October 2026; NOT ACCEPTED):** `repairUnwitnessedTrailingLedgerSuffixLocked()` caught newline write/fsync errors alongside `JSON.parse` errors. A valid, hash-checked event missing only its final LF could be silently truncated if a newline write failed and the snapshot witness was absent. Test-only PR #92 SHA `8fb1e51aca3805b8f83935796b7d47cc5d95f266` reproduced RED on **Ubuntu and Windows** in [CI #41195907240](https://github.com/eybersjp/development-kit/actions/runs/41195907240). Narrow fix separates JSON syntax-error handling from validation and filesystem mutation, preserving valid canonical event bytes and propagating ENOSPC/EIO. Exact code SHA `f5f93e1bb7e350972cd7a8e11892501467724dcf` passed **both complete Ubuntu and Windows** validation jobs in [CI #41196089460](https://github.com/eybersjp/development-kit/actions/runs/41196089460); production acceptance failed closed with `Missing exact-source independent reviews`. Issue #84 contains the test-first evidence. This memory-only commit changes the PR #92 head and requires fresh exact-head CI. Still outstanding: physical NTFS sudden-power-loss Issue #73, genuine distinct code/architecture/security approvals, two final reviewed-source Ubuntu/Windows runs, persisted deterministic `ACCEPTED`, and protected v0.13 release authorization. No merges or waivers. From dcd3e86391e87f0cc9a7480a4bbaaf1d6bba6c9e Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 16:07:54 +0200 Subject: [PATCH 136/177] test(T04-H62): fail closed on state-lock release EIO after durable append --- scripts/state-engine-v2-hardening.test.mjs | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index e0bd3f9a4..64144e3ab 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -1374,3 +1374,35 @@ test('T04-H61 lock metadata write failure must not leave an unowned lock', (t) = assert.equal(loadCanonicalEvents(rootDir).length, 2); assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); }); + + +test('T04-H62 failed state-lock release must not silently acknowledge a durable append', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H62_BASELINE', payload: { step: 1 } }); + const paths = getStateEnginePaths(rootDir); + const originalUnlink = fs.unlinkSync; + let injected = false; + fs.unlinkSync = function failLockRelease(target, ...args) { + if (!injected && path.resolve(String(target)) === path.resolve(paths.lock)) { + injected = true; + const error = new Error('injected state-lock release unlink EIO'); + error.code = 'EIO'; + throw error; + } + return originalUnlink.call(this, target, ...args); + }; + try { + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'H62_COMMITTED', payload: { step: 2 } }), + /lock release|offline recovery|EIO/i, + 'A failed release of the owned lock must never return an unqualified success', + ); + } finally { + fs.unlinkSync = originalUnlink; + } + assert.equal(injected, true, 'Fault must hit the actual state-lock release'); + assert.equal(fs.existsSync(paths.lock), true, 'Failed lock release must preserve its owner witness'); + assert.equal(loadCanonicalEvents(rootDir).length, 2, + 'A durable canonical append must not be rolled back or duplicated after release failure'); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From 369b8d0ddb67592aa7291e59e9cc344a85d754aa Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 16:08:48 +0200 Subject: [PATCH 137/177] fix(T04-H62): propagate state-lock release faults without false commit success --- runtime/orchestration/state-engine-v2.mjs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index c2f4074c4..5e4e83c7c 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -593,10 +593,21 @@ export function recoverAbandonedStateLock({ rootDir = process.cwd(), confirmOffl function releaseStateLock(lock) { if (!lock?.path) return; try { - if (!fs.existsSync(lock.path)) return; const current = JSON.parse(fs.readFileSync(lock.path, 'utf8')); - if (current.owner === lock.owner) fs.unlinkSync(lock.path); - } catch {} + if (current?.owner !== lock.owner) { + throw new StateEngineError('State Engine lock ownership changed before release'); + } + fs.unlinkSync(lock.path); + } catch (error) { + // A completed canonical append may already be durable. Never report + // unqualified success if its lock was not demonstrably released, and never + // silently authorize a blind retry that could duplicate committed work. + // Missing/replaced/corrupt locks and I/O failures require offline review. + throw new StateEngineError( + 'State Engine lock release could not be verified; canonical commit may already be durable; offline recovery required', + { code: 'STATE_LOCK_RELEASE_FAILED', cause: error.code || error.message }, + ); + } } function schemaDocument() { From 78d9e22890671883c6566174502bfba9fac020c6 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 16:10:23 +0200 Subject: [PATCH 138/177] fix(T04-H62): retain primary transaction error when lock cleanup also fails --- runtime/orchestration/state-engine-v2.mjs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 5e4e83c7c..db655b8a1 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -1686,12 +1686,29 @@ function withStateLock(rootDir, task) { if (ACTIVE_STATE_LOCKS.has(key)) return task(); const lock = acquireStateLock(rootDir); ACTIVE_STATE_LOCKS.add(key); + let primaryError = null; try { recoverPendingCommitLocked(rootDir); return task(); + } catch (error) { + primaryError = error; + throw error; } finally { ACTIVE_STATE_LOCKS.delete(key); - releaseStateLock(lock); + try { + releaseStateLock(lock); + } catch (releaseError) { + if (primaryError) { + // A failed transaction must retain its original filesystem/security + // cause. Record a secondary release failure without replacing the + // primary error or silently claiming the lock was cleaned up. + if (primaryError && typeof primaryError === 'object') { + primaryError.lockReleaseError = releaseError; + } + } else { + throw releaseError; + } + } } } From dd07080276dfb3d50d6a7f6504765278f2037a5e Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 16:17:22 +0200 Subject: [PATCH 139/177] docs(memory): record T04-H62 red/green lock-release evidence and remaining gates --- memory.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/memory.md b/memory.md index 855e5e5a1..e867c1700 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,10 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) + +- **T04-H62 lock-release EIO false-success and primary-error masking (31 October 2026; NOT ACCEPTED):** A durable canonical append could return an unqualified success despite `releaseStateLock()` silently swallowing EIO while removing its owned lock. Fault-injection regression added first on authoritative PR #92 test-only SHA `dcd3e86391e87f0cc9a7480a4bbaaf1d6bba6c9e`; both Ubuntu and Windows independently reproduced `Missing expected exception` in [CI #41208705450](https://github.com/eybersjp/development-kit/actions/runs/41208705450) (RED). The first source correction `369b8d0ddb67592aa7291e59e9cc344a85d754aa` made release failures explicit, but existing H36/H37/H38/H40 adversarial directory-swap tests exposed secondary lock-release errors masking the primary security failure (Ubuntu [CI #41208773372](https://github.com/eybersjp/development-kit/actions/runs/41208773372), RED). Narrow follow-up `78d9e22890671883c6566174502bfba9fac020c6` preserves the original transaction exception and attaches a secondary lock-release error; a successful durable append with failed release instead throws an explicit offline-recovery error warning that the commit may already be durable and must not be blindly retried. Ubuntu full v0.13 validation PASS in [CI #41208902511](https://github.com/eybersjp/development-kit/actions/runs/41208902511); Windows validation was still in progress when this memory entry was written. This memory-only update requires fresh final-SHA CI. Physical NTFS power-loss evidence (Issue #73), distinct exact-source independent code/architecture/security approvals, canonical persisted `ACCEPTED`, and protected v0.13 release approval remain outstanding. No gate waivers or merges. + + - **T04-H61 lock initialization I/O failure (31 October 2026; NOT ACCEPTED):** A newly created exclusive state-lock file was left behind if its metadata write or fsync failed (EIO/ENOSPC), blocking later writers with an unowned lock. Isolated draft diagnostic [PR #141](https://github.com/eybersjp/development-kit/pull/141) added two fault-injection regressions first: SHA `a641b446cf9de9d93fdf2995d1fb0169ca81964d` reproduced both RED on Ubuntu and Windows in [CI #41204160210](https://github.com/eybersjp/development-kit/actions/runs/41204160210). Narrow fix records the exclusive lock inode, closes its descriptor, removes only the same single-link file after failed initialization, and flushes the parent; unexpected replacement/cleanup failures remain explicit offline-recovery errors. Diagnostic SHA `109a75667b0af1d871e6f7b013bd23f726738919` passed both complete Ubuntu/Windows validation jobs in [CI #41204273231](https://github.com/eybersjp/development-kit/actions/runs/41204273231). Only validated test and source files were ported to authoritative draft PR #92 in commits `176869e17cd3b9c21b2e5fdb18bd91111b821ecc` and `21c1ae757d3cfb993024c78261ac092d03e016b1`. This memory update changes the exact integration SHA, requiring fresh two-run cross-platform CI. No review approval, physical NTFS power-loss proof, ACCEPTED or release is claimed; Issues #71/#73/#84 and protected v0.13 gates remain binding. From 586fa89b4c703e9887f18be6c39fd51afc3b967b Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 17:14:44 +0200 Subject: [PATCH 140/177] test(t04-h63): preserve acknowledged snapshot on read EACCES/EIO --- scripts/state-engine-v2-hardening.test.mjs | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index 64144e3ab..afb127c20 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -12,6 +12,7 @@ import { getStateEnginePaths, loadCanonicalEvents, loadStateSnapshot, + rebuildStateSnapshot, verifyStateEngineIntegrity, recoverAbandonedStateLock, ensureStateEngineLayout, @@ -1406,3 +1407,47 @@ test('T04-H62 failed state-lock release must not silently acknowledge a durable 'A durable canonical append must not be rolled back or duplicated after release failure'); assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); }); + + +for (const deniedCode of ['EACCES', 'EIO']) { + test(`T04-H63 unreadable acknowledged snapshot blocks derived repair (${deniedCode})`, (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H63_BASELINE', payload: { step: 1 } }); + appendMetadataEvent({ rootDir, eventType: 'H63_ACKNOWLEDGED', payload: { step: 2 } }); + const paths = getStateEnginePaths(rootDir); + const snapshotWitness = fs.readFileSync(paths.snapshot); + const indexWitness = fs.readFileSync(paths.index); + const canonical = fs.readFileSync(paths.events, 'utf8'); + const firstLine = canonical.slice(0, canonical.indexOf('\n') + 1); + fs.writeFileSync(paths.events, firstLine); + const truncatedCanonical = fs.readFileSync(paths.events); + assert.equal(fs.existsSync(paths.pending), false); + + const originalRead = fs.readFileSync; + let injected = 0; + fs.readFileSync = function denyDerivedSnapshotWitness(target, ...args) { + if (String(target) === paths.snapshot) { + injected += 1; + const error = new Error(`Synthetic ${deniedCode} reading acknowledged snapshot`); + error.code = deniedCode; + throw error; + } + return originalRead.call(this, target, ...args); + }; + try { + assert.throws(() => rebuildStateSnapshot(rootDir), + /snapshot witness|EACCES|EIO|cannot inspect/i, + 'A failed witness read must not authorize replacement by an older ledger prefix'); + } finally { + fs.readFileSync = originalRead; + } + assert.ok(injected > 0, 'Fault injection must reach the actual snapshot witness read'); + assert.deepEqual(fs.readFileSync(paths.snapshot), snapshotWitness, + 'The acknowledged snapshot witness must survive failed inspection byte-for-byte'); + assert.deepEqual(fs.readFileSync(paths.index), indexWitness, + 'The derived index must not be rewritten after witness inspection fails'); + assert.deepEqual(fs.readFileSync(paths.events), truncatedCanonical, + 'The canonical ledger must not be modified by rejected derived repair'); + assert.equal(fs.existsSync(paths.pending), false); + }); +} From b02766826cdc6c44dca98f9f2a8dffc33299115d Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 17:14:57 +0200 Subject: [PATCH 141/177] fix(t04-h63): fail closed when acknowledged snapshot witness is unreadable --- runtime/orchestration/state-engine-v2.mjs | 26 +++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index db655b8a1..dd07bd927 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -1313,11 +1313,29 @@ function repairDerivedLocked(rootDir) { const canonical = rebuildSnapshotFromEvents(parseLedger(fs.readFileSync(paths.events, 'utf8'))); if (fs.existsSync(paths.snapshot)) { let witness = null; + let witnessBytes; try { - witness = JSON.parse(fs.readFileSync(paths.snapshot, 'utf8')); - validateStateSnapshot(witness); - } catch { - witness = null; + witnessBytes = fs.readFileSync(paths.snapshot, 'utf8'); + } catch (error) { + // An absent or malformed derived snapshot is rebuildable, but an + // unreadable acknowledged snapshot may witness later canonical history. + // Never erase that evidence after EACCES, EIO or sharing failures. + if (error.code !== 'ENOENT') { + throw new StateEngineError( + 'Cannot inspect acknowledged snapshot witness before derived repair: ' + + (error.code || error.message), + { code: 'SNAPSHOT_WITNESS_READ_FAILED', cause: error.code || error.message }, + ); + } + } + if (witnessBytes !== undefined) { + try { + witness = JSON.parse(witnessBytes); + validateStateSnapshot(witness); + } catch { + // Malformed disposable derived data can be rebuilt from canonical. + witness = null; + } } if (witness && witness.lastEventSequence > canonical.lastEventSequence) { throw new StateEngineError('Canonical state history appears truncated relative to the materialized snapshot'); From dc546a585dc1b43304ab6be745de1d9b4ddb4f41 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 17:15:10 +0200 Subject: [PATCH 142/177] docs(t04-h63): record red-green snapshot witness evidence and Windows H03 retry limitation --- memory.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/memory.md b/memory.md index e867c1700..7629d2ea5 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,9 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-H63 unreadable acknowledged snapshot during derived repair (31 October 2026; NOT ACCEPTED):** `repairDerivedLocked()` previously caught EACCES/EIO from `snapshot.json` reads as though the existing acknowledged-history witness were malformed/disposable, then rewrote the snapshot and index from a potentially truncated canonical prefix. Isolated unmerged diagnostic [PR #144](https://github.com/eybersjp/development-kit/pull/144), test-only SHA `bb2af86868dc9fc95d9fbab5364d36b6b507806c`, reproduced both EACCES and EIO evidence-loss paths on Ubuntu and Windows in [RED CI #41212217175](https://github.com/eybersjp/development-kit/actions/runs/41212217175). Narrow fix separates snapshot filesystem read errors (fail closed except ENOENT) from malformed disposable JSON; source SHA `193380a703a218139240efc649395fac25c81ab6` passed full Ubuntu validation and Windows `T04-H63` regressions in [CI #41212488495](https://github.com/eybersjp/development-kit/actions/runs/41212488495). The first Windows full compatibility attempt failed an unrelated H03 four-worker lock-acquisition timeout; rerun of the failed Windows job **PASSED the full compatibility suite**, including H03 and both H63 regressions. The intermittent H03 timeout warrants investigation; a rerun is not a second independent full workflow execution. Ported only verified H63 test/source to authoritative draft PR #92 commits `586fa89b4c703e9887f18be6c39fd51afc3b967b` and `b02766826cdc6c44dca98f9f2a8dffc33299115d`. This memory commit changes the integration SHA, requiring fresh exact-head CI and two independent complete Ubuntu/Windows runs. Issue #73 controlled physical NTFS sudden-power-loss evidence, three distinct authenticated code/architecture/security approvals, canonical persisted `ACCEPTED` and protected v0.13 release approval remain outstanding. No diagnostic merge, gate waiver or fabricated physical evidence. + + - **T04-H62 lock-release EIO false-success and primary-error masking (31 October 2026; NOT ACCEPTED):** A durable canonical append could return an unqualified success despite `releaseStateLock()` silently swallowing EIO while removing its owned lock. Fault-injection regression added first on authoritative PR #92 test-only SHA `dcd3e86391e87f0cc9a7480a4bbaaf1d6bba6c9e`; both Ubuntu and Windows independently reproduced `Missing expected exception` in [CI #41208705450](https://github.com/eybersjp/development-kit/actions/runs/41208705450) (RED). The first source correction `369b8d0ddb67592aa7291e59e9cc344a85d754aa` made release failures explicit, but existing H36/H37/H38/H40 adversarial directory-swap tests exposed secondary lock-release errors masking the primary security failure (Ubuntu [CI #41208773372](https://github.com/eybersjp/development-kit/actions/runs/41208773372), RED). Narrow follow-up `78d9e22890671883c6566174502bfba9fac020c6` preserves the original transaction exception and attaches a secondary lock-release error; a successful durable append with failed release instead throws an explicit offline-recovery error warning that the commit may already be durable and must not be blindly retried. Ubuntu full v0.13 validation PASS in [CI #41208902511](https://github.com/eybersjp/development-kit/actions/runs/41208902511); Windows validation was still in progress when this memory entry was written. This memory-only update requires fresh final-SHA CI. Physical NTFS power-loss evidence (Issue #73), distinct exact-source independent code/architecture/security approvals, canonical persisted `ACCEPTED`, and protected v0.13 release approval remain outstanding. No gate waivers or merges. From 2636bba99fb0f4b1825ecbbe3ff7d4aff269cb9d Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 17:20:17 +0200 Subject: [PATCH 143/177] test(t04): reproduce security-control CI witness acceptance deadlock (A25) --- scripts/v013-t04-acceptance-ci-contract.test.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs index 14fef90df..8de544914 100644 --- a/scripts/v013-t04-acceptance-ci-contract.test.mjs +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -193,3 +193,18 @@ test('T04-A24 immutable reviewer identity prevents account-rename revocation byp assert.throws(() => assertCurrentRoleApproval(prior, [prior, stale], sha), /superseded|stale|effective/i); }); + + +test('T04-A25 full platform-validation witnesses cannot require the overall run to pass the independent acceptance job', () => { + // The first full runs on a reviewed source legitimately fail the separate + // independent-acceptance job while both full OS validation jobs pass. + // Requiring overall run.conclusion=success here creates an acceptance + // deadlock: security controls cannot cite those otherwise valid runs. + const gate = fs.readFileSync(path.join(root, 'scripts/v013-t04-acceptance-gate.mjs'), 'utf8'); + assert.match(gate, /validationRuns\\.has\\(witness\\.workflowRunId\\)/, + 'A control must still cite an authenticated, exact-source full-validation run'); + assert.match(gate, /assert\\.equal\\(witnessRun\\.head_sha, reviewedCommit\\)/, + 'A control cannot cite a different source'); + assert.doesNotMatch(gate, /witnessRun\\.conclusion\\s*,\\s*'success'/, + 'The overall CI run can fail solely because the separate independent acceptance gate correctly fails closed; validate both OS jobs and mandatory steps instead'); +}); From 75ea6a8e7a5669bd8ff667fdc297397d37ed87ed Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 17:20:32 +0200 Subject: [PATCH 144/177] test(t04): correct A25 negative-control regex literals --- scripts/v013-t04-acceptance-ci-contract.test.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/v013-t04-acceptance-ci-contract.test.mjs b/scripts/v013-t04-acceptance-ci-contract.test.mjs index 8de544914..ea09eb993 100644 --- a/scripts/v013-t04-acceptance-ci-contract.test.mjs +++ b/scripts/v013-t04-acceptance-ci-contract.test.mjs @@ -201,10 +201,10 @@ test('T04-A25 full platform-validation witnesses cannot require the overall run // Requiring overall run.conclusion=success here creates an acceptance // deadlock: security controls cannot cite those otherwise valid runs. const gate = fs.readFileSync(path.join(root, 'scripts/v013-t04-acceptance-gate.mjs'), 'utf8'); - assert.match(gate, /validationRuns\\.has\\(witness\\.workflowRunId\\)/, + assert.match(gate, /validationRuns\.has\(witness\.workflowRunId\)/, 'A control must still cite an authenticated, exact-source full-validation run'); - assert.match(gate, /assert\\.equal\\(witnessRun\\.head_sha, reviewedCommit\\)/, + assert.match(gate, /assert\.equal\(witnessRun\.head_sha, reviewedCommit\)/, 'A control cannot cite a different source'); - assert.doesNotMatch(gate, /witnessRun\\.conclusion\\s*,\\s*'success'/, + assert.doesNotMatch(gate, /witnessRun\.conclusion\s*,\s*'success'/, 'The overall CI run can fail solely because the separate independent acceptance gate correctly fails closed; validate both OS jobs and mandatory steps instead'); }); From 6b7df4e224dbe8b5bd65dafe4a5dfa4a1a1a99be Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 17:21:53 +0200 Subject: [PATCH 145/177] fix(t04): accept exact-source security witnesses from passing OS jobs despite blocked acceptance (A25) --- scripts/v013-t04-acceptance-gate.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/v013-t04-acceptance-gate.mjs b/scripts/v013-t04-acceptance-gate.mjs index bafc8edbb..9d2b05789 100644 --- a/scripts/v013-t04-acceptance-gate.mjs +++ b/scripts/v013-t04-acceptance-gate.mjs @@ -293,7 +293,12 @@ for (const control of requiredControls) { assert.ok((witnessReview.body || '').includes('DKF130-T04 CONTROL ' + control.id + ' ' + reviewedCommit)); const witnessRun = await api('actions/runs/' + witness.workflowRunId); assert.equal(witnessRun.head_sha, reviewedCommit); - assert.equal(witnessRun.conclusion, 'success'); + assert.equal(witnessRun.status, 'completed'); + // The full Ubuntu/Windows validation jobs and mandatory steps were already + // authenticated above for this exact run ID. The separate independent + // acceptance job must fail closed before reviews/physical evidence exist, + // so requiring the *overall* run conclusion to be success would deadlock + // the first legitimate acceptance. Never weaken the per-platform checks. controlResults.push({ id: control.id, status: 'PASS', evidence: [{ type: 'test', deterministicVerification: true, From 0923451bdce6345a4042c055f1e4123db23f336f Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 17:30:45 +0200 Subject: [PATCH 146/177] docs(t04): record A25 test-first RED/GREEN CI and remaining acceptance boundaries --- memory.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/memory.md b/memory.md index 7629d2ea5..13da88a80 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,9 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-A25 independent-acceptance/CI witness deadlock (31 October 2026; NOT ACCEPTED):** In `scripts/v013-t04-acceptance-gate.mjs`, security-control evidence correctly had to cite one of the authenticated exact-SHA, full Ubuntu+Windows validation runs, but also demanded `witnessRun.conclusion === 'success'`. The overall run is expected to **fail** before independent approvals because the separate T04 acceptance job fails closed, even when both full platform validation jobs and mandatory steps succeed; this requirement made first acceptance impossible. Test-first regression on actual draft PR #92 at test-only SHA `75ea6a8e7a5669bd8ff667fdc297397d37ed87ed` reproduced `T04-A25` RED on **both Ubuntu and Windows** in [CI #41214400882](https://github.com/eybersjp/development-kit/actions/runs/41214400882). Narrow fix SHA `6b7df4e224dbe8b5bd65dafe4a5dfa4a1a1a99be` removes only the redundant overall-run conclusion assertion, retaining exact reviewed SHA, authenticated allowed run membership, completed-run state, two distinct workflow IDs, full platform job and mandatory-step success, and all independent review/physical NTFS controls. **Both complete Ubuntu and Windows validation jobs PASS** in [CI #41214508947](https://github.com/eybersjp/development-kit/actions/runs/41214508947); independent T04 acceptance correctly FAILS at missing exact-source reviews. This documentation commit changes the integration HEAD, so final exact-head CI must be repeated. Still outstanding: Issue #73 physical NTFS sudden-power-loss evidence, three distinct authenticated code/architecture/security approvals, complete final-source validations, authentic persisted `ACCEPTED` and protected v0.13 release authorization. No gate waiver, fabricated review, merge or release. + + - **T04-H63 unreadable acknowledged snapshot during derived repair (31 October 2026; NOT ACCEPTED):** `repairDerivedLocked()` previously caught EACCES/EIO from `snapshot.json` reads as though the existing acknowledged-history witness were malformed/disposable, then rewrote the snapshot and index from a potentially truncated canonical prefix. Isolated unmerged diagnostic [PR #144](https://github.com/eybersjp/development-kit/pull/144), test-only SHA `bb2af86868dc9fc95d9fbab5364d36b6b507806c`, reproduced both EACCES and EIO evidence-loss paths on Ubuntu and Windows in [RED CI #41212217175](https://github.com/eybersjp/development-kit/actions/runs/41212217175). Narrow fix separates snapshot filesystem read errors (fail closed except ENOENT) from malformed disposable JSON; source SHA `193380a703a218139240efc649395fac25c81ab6` passed full Ubuntu validation and Windows `T04-H63` regressions in [CI #41212488495](https://github.com/eybersjp/development-kit/actions/runs/41212488495). The first Windows full compatibility attempt failed an unrelated H03 four-worker lock-acquisition timeout; rerun of the failed Windows job **PASSED the full compatibility suite**, including H03 and both H63 regressions. The intermittent H03 timeout warrants investigation; a rerun is not a second independent full workflow execution. Ported only verified H63 test/source to authoritative draft PR #92 commits `586fa89b4c703e9887f18be6c39fd51afc3b967b` and `b02766826cdc6c44dca98f9f2a8dffc33299115d`. This memory commit changes the integration SHA, requiring fresh exact-head CI and two independent complete Ubuntu/Windows runs. Issue #73 controlled physical NTFS sudden-power-loss evidence, three distinct authenticated code/architecture/security approvals, canonical persisted `ACCEPTED` and protected v0.13 release approval remain outstanding. No diagnostic merge, gate waiver or fabricated physical evidence. From 6be7e3e2a13777536904c2999c87939bf36b5b1a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 18:09:26 +0200 Subject: [PATCH 147/177] test(t04): H64 reject missing ledger with acknowledged snapshot witness (RED) --- scripts/state-engine-v2-hardening.test.mjs | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index afb127c20..a846512e8 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -1451,3 +1451,42 @@ for (const deniedCode of ['EACCES', 'EIO']) { assert.equal(fs.existsSync(paths.pending), false); }); } + + +test('T04-H64 a missing canonical ledger cannot be silently recreated over acknowledged snapshot history', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H64_ACKNOWLEDGED', payload: { critical: true } }); + const paths = getStateEnginePaths(rootDir); + const canonicalWitness = fs.readFileSync(paths.events); + const snapshotWitness = fs.readFileSync(paths.snapshot); + const indexWitness = fs.readFileSync(paths.index); + assert.ok(canonicalWitness.length > 0); + assert.ok(JSON.parse(snapshotWitness.toString('utf8')).lastEventSequence > 0); + + // Model loss of the directory entry after a completed, acknowledged commit. + // No pending journal is required to prove this is not a fresh project: + // the durable snapshot/index are existing witnesses of committed history. + fs.unlinkSync(paths.events); + assert.equal(fs.existsSync(paths.pending), false); + + for (const [label, action] of [ + ['canonical reader', () => loadCanonicalEvents(rootDir)], + ['snapshot reader', () => loadStateSnapshot(rootDir)], + ['layout initializer', () => ensureStateEngineLayout(rootDir)], + ['new writer', () => appendMetadataEvent({ + rootDir, eventType: 'H64_MUST_NOT_REPLACE_HISTORY', payload: { critical: false }, + })], + ]) { + assert.throws(action, /missing|absent|incomplete|integrity|canonical|ledger/i, + label + ' must fail closed instead of accepting an empty canonical history'); + assert.equal(fs.existsSync(paths.events), false, + label + ' must not manufacture a replacement canonical ledger'); + assert.deepEqual(fs.readFileSync(paths.snapshot), snapshotWitness); + assert.deepEqual(fs.readFileSync(paths.index), indexWitness); + } + + // The original canonical bytes remain sufficient to restore the authority + // through a separate, evidence-preserving operator recovery operation. + fs.writeFileSync(paths.events, canonicalWitness); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From ed14bbf17f8b9a8ce8d686f05d6a1cc4d17d3e76 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 18:10:28 +0200 Subject: [PATCH 148/177] fix(t04): H64 fail closed on lost canonical ledger with history witnesses --- runtime/orchestration/state-engine-v2.mjs | 32 ++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index dd07bd927..23f772974 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -716,6 +716,26 @@ function createInitialStateFile(filePath, content) { } } +/** + * The canonical ledger must never be reinitialized after a committed history + * has left a snapshot or index witness. An interrupted first initializer may + * have only its schema; published derived state always follows ledger creation. + * Loss of the ledger without a pending journal is still a recovery fault. + */ +function assertMissingCanonicalLedgerHasNoStateWitness(paths) { + if (fs.existsSync(paths.pending)) { + throw new StateEngineError( + 'State Engine events.jsonl is missing while pending-commit.json is present; state integrity compromised', + ); + } + if (fs.existsSync(paths.snapshot) || fs.existsSync(paths.index)) { + throw new StateEngineError( + 'State Engine canonical events.jsonl is missing while snapshot/index history witness remains; offline recovery required', + { code: 'STATE_CANONICAL_LEDGER_MISSING' }, + ); + } +} + export function ensureStateEngineLayout(rootDir = process.cwd()) { const paths = statePaths(rootDir); fs.mkdirSync(paths.stateRoot, { recursive: true }); @@ -728,9 +748,7 @@ export function ensureStateEngineLayout(rootDir = process.cwd()) { } if (!fs.existsSync(paths.events)) { - if (fs.existsSync(paths.pending)) { - throw new StateEngineError('State Engine events.jsonl is missing while pending-commit.json is present; state integrity compromised'); - } + assertMissingCanonicalLedgerHasNoStateWitness(paths); createInitialStateFile(paths.events, ''); } if (!fs.existsSync(paths.snapshot)) { @@ -1007,9 +1025,7 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { export function loadCanonicalEvents(rootDir = process.cwd()) { const paths = statePaths(rootDir); if (!fs.existsSync(paths.events)) { - if (fs.existsSync(paths.pending)) { - throw new StateEngineError('State Engine events.jsonl is missing while pending-commit.json is present; state integrity compromised'); - } + assertMissingCanonicalLedgerHasNoStateWitness(paths); return []; } if (fs.existsSync(paths.pending)) { @@ -1356,9 +1372,7 @@ export function rebuildStateSnapshot(rootDir = process.cwd()) { export function loadStateSnapshot(rootDir = process.cwd(), { rebuildIfNeeded = true } = {}) { const paths = statePaths(rootDir); if (!fs.existsSync(paths.events)) { - if (fs.existsSync(paths.pending)) { - throw new StateEngineError('State Engine events.jsonl is missing while pending-commit.json is present; state integrity compromised'); - } + assertMissingCanonicalLedgerHasNoStateWitness(paths); return null; } const canonical = rebuildSnapshotFromEvents(loadCanonicalEvents(rootDir)); From dad570c4d3de188706583c02bf98a17b054f3762 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 18:17:49 +0200 Subject: [PATCH 149/177] docs(t04): record H64 red/green evidence and remaining risk-3 boundaries --- memory.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/memory.md b/memory.md index 13da88a80..58e695159 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,9 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) + +- **T04-H64 missing canonical ledger with acknowledged derived-history witnesses (31 October 2026; NOT ACCEPTED):** A committed State Engine V2 event could be silently lost if the `events.jsonl` directory entry disappeared without a pending journal while an existing `snapshot.json` and `index.db` still witnessed committed history. `loadCanonicalEvents()` returned an empty array, `loadStateSnapshot()` returned null and `ensureStateEngineLayout()` could recreate an empty canonical ledger. Test-only authoritative draft PR #92 SHA `6be7e3e2a13777536904c2999c87939bf36b5b1a` added adversarial H64 checks for readers, initialization and writes preserving original witnesses. Both Ubuntu and Windows reproduced RED (`Missing expected exception: canonical reader must fail closed`) in [CI #41218300511](https://github.com/eybersjp/development-kit/actions/runs/41218300511). Narrow runtime fix SHA `ed14bbf17f8b9a8ce8d686f05d6a1cc4d17d3e76` refuses to initialize a missing canonical ledger when a pending journal or published snapshot/index remains; only a genuinely witness-free bootstrap can initialize. Ubuntu complete v0.13 validation PASS, Windows H64/state-engine increment PASS in [CI #41218389134](https://github.com/eybersjp/development-kit/actions/runs/41218389134); Windows full compatibility regression was still in progress at this evidence checkpoint. This memory commit changes the final source SHA and requires fresh exact-head CI and independent review. Physical NTFS sudden-power-loss evidence (Issue #73), three distinct authenticated code/architecture/security approvals, two full Ubuntu/Windows runs on the final reviewed SHA, canonical persisted risk-3 `ACCEPTED`, and protected v0.13 release authorization remain mandatory. No waiver, merge or release. + - **T04-A25 independent-acceptance/CI witness deadlock (31 October 2026; NOT ACCEPTED):** In `scripts/v013-t04-acceptance-gate.mjs`, security-control evidence correctly had to cite one of the authenticated exact-SHA, full Ubuntu+Windows validation runs, but also demanded `witnessRun.conclusion === 'success'`. The overall run is expected to **fail** before independent approvals because the separate T04 acceptance job fails closed, even when both full platform validation jobs and mandatory steps succeed; this requirement made first acceptance impossible. Test-first regression on actual draft PR #92 at test-only SHA `75ea6a8e7a5669bd8ff667fdc297397d37ed87ed` reproduced `T04-A25` RED on **both Ubuntu and Windows** in [CI #41214400882](https://github.com/eybersjp/development-kit/actions/runs/41214400882). Narrow fix SHA `6b7df4e224dbe8b5bd65dafe4a5dfa4a1a1a99be` removes only the redundant overall-run conclusion assertion, retaining exact reviewed SHA, authenticated allowed run membership, completed-run state, two distinct workflow IDs, full platform job and mandatory-step success, and all independent review/physical NTFS controls. **Both complete Ubuntu and Windows validation jobs PASS** in [CI #41214508947](https://github.com/eybersjp/development-kit/actions/runs/41214508947); independent T04 acceptance correctly FAILS at missing exact-source reviews. This documentation commit changes the integration HEAD, so final exact-head CI must be repeated. Still outstanding: Issue #73 physical NTFS sudden-power-loss evidence, three distinct authenticated code/architecture/security approvals, complete final-source validations, authentic persisted `ACCEPTED` and protected v0.13 release authorization. No gate waiver, fabricated review, merge or release. From 3316ff1259da0d6b537a9bcc70de9e6e41d39fc5 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 19:43:49 +0200 Subject: [PATCH 150/177] fix(t04-h65): preserve acknowledged derived witnesses across canonical history regression --- memory.md | 2 + runtime/orchestration/state-engine-v2.mjs | 99 ++++++++----- scripts/state-engine-v2-hardening.test.mjs | 154 +++++++++++++++++++++ 3 files changed, 221 insertions(+), 34 deletions(-) diff --git a/memory.md b/memory.md index 58e695159..e9bc8a50b 100644 --- a/memory.md +++ b/memory.md @@ -11,6 +11,8 @@ ## v0.13.0 P0 — State Engine V2 T04 (30 October 2026) +- **T04-H65 valid-prefix canonical truncation versus acknowledged derived witnesses (31 October 2026; NOT ACCEPTED):** A valid, newline-terminated but truncated `events.jsonl` prefix could be accepted by `appendMetadataEvent()` and a replacement event could overwrite an acknowledged sequence, silently rewriting `snapshot.json` and `index.db`. When the snapshot was absent, `repairDerivedLocked()` could also overwrite an index that witnessed later history. Isolated, unmerged [diagnostic PR #146](https://github.com/eybersjp/development-kit/pull/146) added fail-first H65 controls; test-only SHA `0d5e7b637eed9258bfa5d96aff5d8a4a88757b8f` reproduced three missing fail-closed exceptions on **both Ubuntu and Windows** in [RED CI #41225028114](https://github.com/eybersjp/development-kit/actions/runs/41225028114). Narrow correction compares validated snapshot/index sequence and hash to the **original pre-append canonical prefix** before pending-journal publication, derived repair, torn-tail truncation or pending-recovery mutation; EIO/EACCES reading witnesses fails closed, while genuinely older consistent derived views remain rebuildable. Diagnostic source/test `16ac782045401bbb9255ebeacfb344fde8034a5d` passed **both full Ubuntu and Windows** jobs in [CI #41225229774](https://github.com/eybersjp/development-kit/actions/runs/41225229774). Final diagnostic `7522c4455b748b9c59936a3a8b4bcd8567711735` adds a sixth pending-recovery/index-only adversarial control: Ubuntu full validation and Windows State Engine increment passed in [CI #41225295603](https://github.com/eybersjp/development-kit/actions/runs/41225295603); Windows full compatibility was still running at this evidence checkpoint. Ported only this tested runtime and six regressions, plus this memory record, to authoritative draft PR #92; **fresh exact-port CI and two complete same-final-SHA runs are required**. Issue #73 controlled physical NTFS sudden-power-loss proof, three distinct authenticated exact-source code/architecture/security approvals, persisted risk-3 `ACCEPTED`, and protected v0.13 release approval remain outstanding. No waiver, merge or release. + - **T04-H64 missing canonical ledger with acknowledged derived-history witnesses (31 October 2026; NOT ACCEPTED):** A committed State Engine V2 event could be silently lost if the `events.jsonl` directory entry disappeared without a pending journal while an existing `snapshot.json` and `index.db` still witnessed committed history. `loadCanonicalEvents()` returned an empty array, `loadStateSnapshot()` returned null and `ensureStateEngineLayout()` could recreate an empty canonical ledger. Test-only authoritative draft PR #92 SHA `6be7e3e2a13777536904c2999c87939bf36b5b1a` added adversarial H64 checks for readers, initialization and writes preserving original witnesses. Both Ubuntu and Windows reproduced RED (`Missing expected exception: canonical reader must fail closed`) in [CI #41218300511](https://github.com/eybersjp/development-kit/actions/runs/41218300511). Narrow runtime fix SHA `ed14bbf17f8b9a8ce8d686f05d6a1cc4d17d3e76` refuses to initialize a missing canonical ledger when a pending journal or published snapshot/index remains; only a genuinely witness-free bootstrap can initialize. Ubuntu complete v0.13 validation PASS, Windows H64/state-engine increment PASS in [CI #41218389134](https://github.com/eybersjp/development-kit/actions/runs/41218389134); Windows full compatibility regression was still in progress at this evidence checkpoint. This memory commit changes the final source SHA and requires fresh exact-head CI and independent review. Physical NTFS sudden-power-loss evidence (Issue #73), three distinct authenticated code/architecture/security approvals, two full Ubuntu/Windows runs on the final reviewed SHA, canonical persisted risk-3 `ACCEPTED`, and protected v0.13 release authorization remain mandatory. No waiver, merge or release. diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 23f772974..2c089b7e8 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -894,6 +894,54 @@ function acknowledgedSnapshotWitness(paths) { } } +/** + * A derived snapshot or index is disposable only when it does not witness a + * later or different acknowledged canonical history. Compare against the + * ledger prefix BEFORE creating a new pending journal or rewriting derived + * files; otherwise a valid truncated prefix could be silently re-acknowledged. + * Read failures are not evidence of absence. Malformed derived JSON remains + * rebuildable, as it cannot establish a trustworthy history witness. + */ +function assertDerivedHistoryWitnesses(paths, canonicalSequence, canonicalHash, fields = ['snapshot', 'index']) { + for (const field of fields) { + let bytes; + try { + bytes = fs.readFileSync(paths[field], 'utf8'); + } catch (error) { + if (error.code === 'ENOENT') continue; + throw new StateEngineError( + 'Cannot inspect acknowledged ' + field + ' witness before canonical mutation: ' + + (error.code || error.message), + { code: 'STATE_DERIVED_WITNESS_READ_FAILED', field, cause: error.code || error.message }, + ); + } + let witness; + try { + witness = JSON.parse(bytes); + if (field === 'snapshot') validateStateSnapshot(witness); + else validateStateIndex(witness); + } catch { + // Corrupt derived JSON/schema cannot authorize a history regression. + // It is rebuildable from the validated canonical ledger. + continue; + } + const sequence = field === 'snapshot' ? witness.lastEventSequence : witness.sourceSequence; + const hash = field === 'snapshot' ? witness.lastEventHash : witness.sourceHash; + if (sequence > canonicalSequence) { + throw new StateEngineError( + 'Canonical state history appears truncated relative to the materialized ' + field + ' witness', + { code: 'STATE_CANONICAL_HISTORY_REGRESSION', field, canonicalSequence, witnessedSequence: sequence }, + ); + } + if (sequence === canonicalSequence && hash !== canonicalHash) { + throw new StateEngineError( + 'Canonical state history hash differs from the materialized ' + field + ' integrity witness', + { code: 'STATE_CANONICAL_HISTORY_HASH_CONFLICT', field, canonicalSequence, witnessedHash: hash }, + ); + } + } +} + function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { const paths = ensureStateEngineLayout(rootDir); if (fs.existsSync(paths.pending)) { @@ -1011,6 +1059,11 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { } } + // An index can be the sole surviving acknowledged-history witness when the + // snapshot is absent or malformed. It must also block destructive truncation. + assertDerivedHistoryWitnesses( + paths, prefixEvents.length, prefixEvents.at(-1)?.eventHash ?? null, ['index'], + ); const fd = openVerifiedCanonicalTailRepairFd(); try { fs.ftruncateSync(fd, Buffer.byteLength(prefixContent, 'utf8')); @@ -1327,40 +1380,7 @@ function repairDerivedLocked(rootDir) { // stale cached history from before another writer committed a later event. const paths = ensureStateEngineLayout(rootDir); const canonical = rebuildSnapshotFromEvents(parseLedger(fs.readFileSync(paths.events, 'utf8'))); - if (fs.existsSync(paths.snapshot)) { - let witness = null; - let witnessBytes; - try { - witnessBytes = fs.readFileSync(paths.snapshot, 'utf8'); - } catch (error) { - // An absent or malformed derived snapshot is rebuildable, but an - // unreadable acknowledged snapshot may witness later canonical history. - // Never erase that evidence after EACCES, EIO or sharing failures. - if (error.code !== 'ENOENT') { - throw new StateEngineError( - 'Cannot inspect acknowledged snapshot witness before derived repair: ' + - (error.code || error.message), - { code: 'SNAPSHOT_WITNESS_READ_FAILED', cause: error.code || error.message }, - ); - } - } - if (witnessBytes !== undefined) { - try { - witness = JSON.parse(witnessBytes); - validateStateSnapshot(witness); - } catch { - // Malformed disposable derived data can be rebuilt from canonical. - witness = null; - } - } - if (witness && witness.lastEventSequence > canonical.lastEventSequence) { - throw new StateEngineError('Canonical state history appears truncated relative to the materialized snapshot'); - } - if (witness && witness.lastEventSequence === canonical.lastEventSequence - && witness.lastEventHash !== canonical.lastEventHash) { - throw new StateEngineError('Canonical state history hash differs from the materialized snapshot integrity witness'); - } - } + assertDerivedHistoryWitnesses(paths, canonical.lastEventSequence, canonical.lastEventHash); persistDerived(canonical, rootDir); return canonical; } @@ -1679,6 +1699,11 @@ function recoverPendingCommitLocked(rootDir) { && witness.lastEventHash !== canonical.lastEventHash) { throw new StateEngineError('Pending recovery conflicts with snapshot integrity witness'); } + // A surviving index may witness later committed history even when the + // snapshot is missing or corrupt. Verify it before any ledger mutation. + assertDerivedHistoryWitnesses( + paths, canonical.lastEventSequence, canonical.lastEventHash, ['index'], + ); if (existingSuffix.length < expectedSuffix.length) { // Truncate and repair through this FD, never a separately resolved path. fs.ftruncateSync(fd, pending.previousByteLength); @@ -1796,6 +1821,12 @@ function commitPreparedEventsLocked(newEvents, rootDir = process.cwd()) { existingContent = bytes.toString('utf8'); const existing = parseLedger(existingContent); + // Inspect both acknowledged derived witnesses against the original + // canonical prefix, not the proposed post-append sequence: a new event + // must never conceal a valid truncation of previously accepted history. + assertDerivedHistoryWitnesses( + paths, existing.length, existing.at(-1)?.eventHash ?? null, + ); let previous = existing.at(-1) ?? null; for (const event of newEvents) { validateStateEvent(event, previous); diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index a846512e8..59ee66003 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -1490,3 +1490,157 @@ test('T04-H64 a missing canonical ledger cannot be silently recreated over ackno fs.writeFileSync(paths.events, canonicalWitness); assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); }); + +test('T04-H65 valid canonical prefix truncation cannot overwrite acknowledged snapshot history', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H65_FIRST', payload: { order: 1 } }); + appendMetadataEvent({ rootDir, eventType: 'H65_ACKNOWLEDGED', payload: { order: 2 } }); + const paths = getStateEnginePaths(rootDir); + const fullLedger = fs.readFileSync(paths.events, 'utf8'); + const snapshotWitness = fs.readFileSync(paths.snapshot); + const indexWitness = fs.readFileSync(paths.index); + const prefix = fullLedger.slice(0, fullLedger.indexOf('\n') + 1); + fs.writeFileSync(paths.events, prefix, 'utf8'); + assert.equal(fs.existsSync(paths.pending), false); + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'H65_MUST_NOT_REPLACE', payload: { order: 999 } }), + /canonical|witness|history|truncat|integrity/i, + 'A new valid event must not replace an already acknowledged event with the same sequence', + ); + assert.equal(fs.readFileSync(paths.events, 'utf8'), prefix); + assert.deepEqual(fs.readFileSync(paths.snapshot), snapshotWitness); + assert.deepEqual(fs.readFileSync(paths.index), indexWitness); + assert.equal(fs.existsSync(paths.pending), false); +}); + +test('T04-H65 an index-only witness blocks derived repair and a new entity write over truncated history', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H65_FIRST', payload: { order: 1 } }); + appendMetadataEvent({ rootDir, eventType: 'H65_ACKNOWLEDGED', payload: { order: 2 } }); + const paths = getStateEnginePaths(rootDir); + const fullLedger = fs.readFileSync(paths.events, 'utf8'); + const indexWitness = fs.readFileSync(paths.index); + const prefix = fullLedger.slice(0, fullLedger.indexOf('\n') + 1); + fs.writeFileSync(paths.events, prefix, 'utf8'); + fs.unlinkSync(paths.snapshot); + assert.throws( + () => appendEntityState({ + rootDir, entityType: 'h65-state', entityId: 'test', state: { stateRevision: 1, value: 'must-not-write' }, + }), + /canonical|witness|history|truncat|integrity/i, + 'A retained index is an acknowledged history witness even when the snapshot is missing', + ); + assert.equal(fs.readFileSync(paths.events, 'utf8'), prefix); + assert.deepEqual(fs.readFileSync(paths.index), indexWitness); + // Layout bootstrap may recreate a disposable empty snapshot before it + // inspects the retained index; it must not publish the truncated history. + if (fs.existsSync(paths.snapshot)) { + assert.equal(JSON.parse(fs.readFileSync(paths.snapshot, 'utf8')).lastEventSequence, 0); + } + assert.equal(fs.existsSync(paths.pending), false); +}); + +test('T04-H65 older but consistent derived witnesses may be refreshed by a later valid append', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H65_FIRST', payload: { order: 1 } }); + const paths = getStateEnginePaths(rootDir); + const oldSnapshot = fs.readFileSync(paths.snapshot); + const oldIndex = fs.readFileSync(paths.index); + appendMetadataEvent({ rootDir, eventType: 'H65_SECOND', payload: { order: 2 } }); + fs.writeFileSync(paths.snapshot, oldSnapshot); + fs.writeFileSync(paths.index, oldIndex); + appendMetadataEvent({ rootDir, eventType: 'H65_THIRD', payload: { order: 3 } }); + assert.equal(loadCanonicalEvents(rootDir).length, 3); + assert.equal(JSON.parse(fs.readFileSync(paths.snapshot, 'utf8')).lastEventSequence, 3); + assert.equal(JSON.parse(fs.readFileSync(paths.index, 'utf8')).sourceSequence, 3); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + +test('T04-H65 a failed snapshot witness read cannot authorize a metadata append', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H65_FIRST', payload: { order: 1 } }); + const paths = getStateEnginePaths(rootDir); + const originalLedger = fs.readFileSync(paths.events); + const originalIndex = fs.readFileSync(paths.index); + const originalRead = fs.readFileSync; + let denied = false; + fs.readFileSync = function rejectSnapshotWitnessRead(target, ...args) { + if (String(target) === paths.snapshot) { + denied = true; + const error = new Error('Injected snapshot witness EIO'); + error.code = 'EIO'; + throw error; + } + return originalRead.call(this, target, ...args); + }; + try { + assert.throws( + () => appendMetadataEvent({ rootDir, eventType: 'H65_MUST_NOT_COMMIT', payload: { order: 2 } }), + /snapshot|witness|EIO|inspect/i, + ); + } finally { + fs.readFileSync = originalRead; + } + assert.equal(denied, true, 'Writer must actually inspect the acknowledged snapshot witness'); + assert.deepEqual(fs.readFileSync(paths.events), originalLedger); + assert.deepEqual(fs.readFileSync(paths.index), originalIndex); + assert.equal(fs.existsSync(paths.pending), false); +}); + +test('T04-H65 index-only acknowledged history prevents destructive torn-tail recovery', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H65_FIRST', payload: { order: 1 } }); + appendMetadataEvent({ rootDir, eventType: 'H65_ACKNOWLEDGED', payload: { order: 2 } }); + const paths = getStateEnginePaths(rootDir); + const ledger = fs.readFileSync(paths.events, 'utf8'); + const indexWitness = fs.readFileSync(paths.index); + const first = ledger.slice(0, ledger.indexOf('\n') + 1); + const torn = first + '{"schemaVersion":"1.0.0","sequence":2,'; + fs.writeFileSync(paths.events, torn, 'utf8'); + fs.unlinkSync(paths.snapshot); + assert.throws( + () => loadCanonicalEvents(rootDir), + /index|witness|canonical|truncat|history/i, + 'The surviving index must prevent discarding bytes potentially belonging to an acknowledged event', + ); + assert.equal(fs.readFileSync(paths.events, 'utf8'), torn); + assert.deepEqual(fs.readFileSync(paths.index), indexWitness); + assert.equal(fs.existsSync(paths.pending), false); +}); + +test('T04-H65 index-only witness rejects a conflicting valid pending-recovery suffix', (t) => { + const rootDir = tempProject(t); + const alternativeRoot = tempProject(t); + const at = '2026-10-31T00:00:00.000Z'; + for (const root of [rootDir, alternativeRoot]) { + appendMetadataEvent({ rootDir: root, eventType: 'H65_FIRST', payload: { order: 1 }, timestamp: at }); + } + appendMetadataEvent({ rootDir, eventType: 'H65_ACKNOWLEDGED', payload: { order: 2 }, timestamp: at }); + appendMetadataEvent({ + rootDir: alternativeRoot, eventType: 'H65_CONFLICTING', payload: { order: 999 }, timestamp: at, + }); + const paths = getStateEnginePaths(rootDir); + const fullLedger = fs.readFileSync(paths.events, 'utf8'); + const alternativeLedger = fs.readFileSync(getStateEnginePaths(alternativeRoot).events, 'utf8'); + const prefix = fullLedger.slice(0, fullLedger.indexOf('\n') + 1); + assert.ok(alternativeLedger.startsWith(prefix), 'Both histories must share the exact valid canonical prefix'); + const suffix = alternativeLedger.slice(prefix.length); + const indexWitness = fs.readFileSync(paths.index); + fs.writeFileSync(paths.events, prefix, 'utf8'); + fs.unlinkSync(paths.snapshot); + const pending = JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(prefix), + previousHash: 'sha256:' + createHash('sha256').update(prefix).digest('hex'), + suffix, + }); + fs.writeFileSync(paths.pending, pending, 'utf8'); + assert.throws( + () => loadCanonicalEvents(rootDir), + /index|witness|canonical|history|conflict|hash/i, + 'A hash-valid but different event must not supersede acknowledged index history', + ); + assert.equal(fs.readFileSync(paths.events, 'utf8'), prefix); + assert.deepEqual(fs.readFileSync(paths.index), indexWitness); + assert.equal(fs.readFileSync(paths.pending, 'utf8'), pending); +}); From b7795bb570c87eb9bafb5afc6fe579e2ac5022b2 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 19:58:01 +0200 Subject: [PATCH 151/177] fix(t04-h66): preserve canonical prefix witness during index rebuild and recovery --- memory.md | 2 + runtime/orchestration/state-engine-v2.mjs | 58 +++++++++----- scripts/state-engine-v2-hardening.test.mjs | 90 ++++++++++++++++++++++ 3 files changed, 129 insertions(+), 21 deletions(-) diff --git a/memory.md b/memory.md index e9bc8a50b..3075d8b43 100644 --- a/memory.md +++ b/memory.md @@ -287,3 +287,5 @@ Integrated through PR #54; Issue #51 is completed. - DKF remains general-purpose and provider/host/domain neutral. No application-specific cache or capsule semantics entered core. - v0.12.0 remains unreleased; package version remains 0.11.2; T03 acceptance does not merge to `main`, tag, publish, or release. - Next validated increment: **DKF120-T04 — State Engine V2**. T04 requires its own Development Contract before production implementation. + +- **T04-H66 source-bound witness hardening (31 October 2026; NOT ACCEPTED):** Exact PR #92 SHA `3316ff1259da0d6b537a9bcc70de9e6e41d39fc5` was found by genuine independent Codex review to allow a reader-triggered index rebuild to overwrite later acknowledged history, and pending recovery to ignore conflicting older witness hashes. Isolated draft diagnostic PR #148 test-only SHA `833680235862671ec7c9815a54ba6f1f5758f778` reproduced H66 failures on Ubuntu and Windows in RED CI `41226287003`. Narrow fix SHA `b9a13b6aa3f42fffb15371c906ef0383bd28fd38` passed complete Ubuntu and Windows validation in GREEN CI `41226384214`. Compare each derived witness hash with the canonical event at the witness's own sequence; check both witnesses before rebuilding an index; fail closed on unreadable evidence. Exact-source independent reviews, physical NTFS power-loss assurance and risk-3 ACCEPTED remain outstanding; no merge or release authorized. diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 2c089b7e8..60a767b19 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -902,7 +902,10 @@ function acknowledgedSnapshotWitness(paths) { * Read failures are not evidence of absence. Malformed derived JSON remains * rebuildable, as it cannot establish a trustworthy history witness. */ -function assertDerivedHistoryWitnesses(paths, canonicalSequence, canonicalHash, fields = ['snapshot', 'index']) { +function assertDerivedHistoryWitnesses(paths, canonicalEvents, fields = ['snapshot', 'index']) { + if (!Array.isArray(canonicalEvents)) { + throw new StateEngineError('Canonical events are required for source-bound derived witness validation'); + } for (const field of fields) { let bytes; try { @@ -921,22 +924,28 @@ function assertDerivedHistoryWitnesses(paths, canonicalSequence, canonicalHash, if (field === 'snapshot') validateStateSnapshot(witness); else validateStateIndex(witness); } catch { - // Corrupt derived JSON/schema cannot authorize a history regression. - // It is rebuildable from the validated canonical ledger. + // Malformed derived JSON/schema is rebuildable; unreadable evidence is not. continue; } const sequence = field === 'snapshot' ? witness.lastEventSequence : witness.sourceSequence; const hash = field === 'snapshot' ? witness.lastEventHash : witness.sourceHash; - if (sequence > canonicalSequence) { + if (sequence > canonicalEvents.length) { throw new StateEngineError( 'Canonical state history appears truncated relative to the materialized ' + field + ' witness', - { code: 'STATE_CANONICAL_HISTORY_REGRESSION', field, canonicalSequence, witnessedSequence: sequence }, + { code: 'STATE_CANONICAL_HISTORY_REGRESSION', field, + canonicalSequence: canonicalEvents.length, witnessedSequence: sequence }, ); } - if (sequence === canonicalSequence && hash !== canonicalHash) { + // A witness may be older than the proposed ledger after a legitimate + // append, but only when it agrees with the hash at its OWN sequence. + // Comparing only the proposed final hash would miss a conflicting event + // followed by later valid events (T04-H66). + const prefixHash = sequence === 0 ? null : canonicalEvents[sequence - 1].eventHash; + if (hash !== prefixHash) { throw new StateEngineError( 'Canonical state history hash differs from the materialized ' + field + ' integrity witness', - { code: 'STATE_CANONICAL_HISTORY_HASH_CONFLICT', field, canonicalSequence, witnessedHash: hash }, + { code: 'STATE_CANONICAL_HISTORY_HASH_CONFLICT', field, + witnessedSequence: sequence, witnessedHash: hash, canonicalPrefixHash: prefixHash }, ); } } @@ -1058,12 +1067,17 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { throw new StateEngineError('Materialized snapshot hash disagrees with recoverable canonical prefix'); } } + if (witness && witness.lastEventSequence < prefixEvents.length) { + const witnessedHash = witness.lastEventSequence === 0 + ? null : prefixEvents[witness.lastEventSequence - 1].eventHash; + if (witness.lastEventHash !== witnessedHash) { + throw new StateEngineError('Materialized snapshot hash disagrees with its recoverable canonical prefix'); + } + } // An index can be the sole surviving acknowledged-history witness when the // snapshot is absent or malformed. It must also block destructive truncation. - assertDerivedHistoryWitnesses( - paths, prefixEvents.length, prefixEvents.at(-1)?.eventHash ?? null, ['index'], - ); + assertDerivedHistoryWitnesses(paths, prefixEvents, ['index']); const fd = openVerifiedCanonicalTailRepairFd(); try { fs.ftruncateSync(fd, Buffer.byteLength(prefixContent, 'utf8')); @@ -1379,8 +1393,9 @@ function repairDerivedLocked(rootDir) { // Snapshot repair replays the *current* ledger under the writer lock, never a // stale cached history from before another writer committed a later event. const paths = ensureStateEngineLayout(rootDir); - const canonical = rebuildSnapshotFromEvents(parseLedger(fs.readFileSync(paths.events, 'utf8'))); - assertDerivedHistoryWitnesses(paths, canonical.lastEventSequence, canonical.lastEventHash); + const canonicalEvents = parseLedger(fs.readFileSync(paths.events, 'utf8')); + const canonical = rebuildSnapshotFromEvents(canonicalEvents); + assertDerivedHistoryWitnesses(paths, canonicalEvents); persistDerived(canonical, rootDir); return canonical; } @@ -1453,8 +1468,12 @@ export function loadStateSnapshot(rootDir = process.cwd(), { rebuildIfNeeded = t export function rebuildStateIndex(rootDir = process.cwd()) { return withStateLock(rootDir, () => { - const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }) ?? emptySnapshot(); const paths = ensureStateEngineLayout(rootDir); + // A reader-triggered rebuild is still a filesystem mutation. Preserve an + // existing index's acknowledged-history witness before replacing it. + const canonicalEvents = loadCanonicalEvents(rootDir); + assertDerivedHistoryWitnesses(paths, canonicalEvents); + const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }) ?? emptySnapshot(); const index = buildStateIndex(snapshot); atomicWrite(paths.index, stablePretty(index)); return index; @@ -1662,9 +1681,10 @@ function recoverPendingCommitLocked(rootDir) { if (!expectedSuffix.subarray(0, existingSuffix.length).equals(existingSuffix)) { throw new StateEngineError('Canonical state suffix conflicts with pending commit; refusing recovery'); } - canonical = rebuildSnapshotFromEvents( - parseLedger(Buffer.concat([bytes.subarray(0, pending.previousByteLength), expectedSuffix]).toString('utf8')), + const proposedEvents = parseLedger( + Buffer.concat([bytes.subarray(0, pending.previousByteLength), expectedSuffix]).toString('utf8'), ); + canonical = rebuildSnapshotFromEvents(proposedEvents); // Validate the entire proposed ledger and the materialized snapshot witness // before any truncate/append/fsync can mutate canonical crash evidence. let witness = null; @@ -1701,9 +1721,7 @@ function recoverPendingCommitLocked(rootDir) { } // A surviving index may witness later committed history even when the // snapshot is missing or corrupt. Verify it before any ledger mutation. - assertDerivedHistoryWitnesses( - paths, canonical.lastEventSequence, canonical.lastEventHash, ['index'], - ); + assertDerivedHistoryWitnesses(paths, proposedEvents); if (existingSuffix.length < expectedSuffix.length) { // Truncate and repair through this FD, never a separately resolved path. fs.ftruncateSync(fd, pending.previousByteLength); @@ -1824,9 +1842,7 @@ function commitPreparedEventsLocked(newEvents, rootDir = process.cwd()) { // Inspect both acknowledged derived witnesses against the original // canonical prefix, not the proposed post-append sequence: a new event // must never conceal a valid truncation of previously accepted history. - assertDerivedHistoryWitnesses( - paths, existing.length, existing.at(-1)?.eventHash ?? null, - ); + assertDerivedHistoryWitnesses(paths, existing); let previous = existing.at(-1) ?? null; for (const event of newEvents) { validateStateEvent(event, previous); diff --git a/scripts/state-engine-v2-hardening.test.mjs b/scripts/state-engine-v2-hardening.test.mjs index 59ee66003..98561ddf7 100644 --- a/scripts/state-engine-v2-hardening.test.mjs +++ b/scripts/state-engine-v2-hardening.test.mjs @@ -12,6 +12,7 @@ import { getStateEnginePaths, loadCanonicalEvents, loadStateSnapshot, + loadStateIndex, rebuildStateSnapshot, verifyStateEngineIntegrity, recoverAbandonedStateLock, @@ -1644,3 +1645,92 @@ test('T04-H65 index-only witness rejects a conflicting valid pending-recovery su assert.deepEqual(fs.readFileSync(paths.index), indexWitness); assert.equal(fs.readFileSync(paths.pending, 'utf8'), pending); }); + +test('T04-H66 index rebuild cannot erase a later acknowledged witness behind a valid prefix', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H66_FIRST', payload: { order: 1 } }); + const paths = getStateEnginePaths(rootDir); + const prefix = fs.readFileSync(paths.events, 'utf8'); + const oldSnapshot = fs.readFileSync(paths.snapshot); + appendMetadataEvent({ rootDir, eventType: 'H66_ACKNOWLEDGED', payload: { order: 2 } }); + const indexWitness = fs.readFileSync(paths.index); + fs.writeFileSync(paths.events, prefix, 'utf8'); + fs.writeFileSync(paths.snapshot, oldSnapshot); + assert.throws(() => loadStateIndex(rootDir), /index|witness|canonical|history|truncat|integrity/i, + 'Index repair must not overwrite a later acknowledged index with an older canonical prefix'); + assert.equal(fs.readFileSync(paths.events, 'utf8'), prefix); + assert.deepEqual(fs.readFileSync(paths.index), indexWitness); + assert.deepEqual(fs.readFileSync(paths.snapshot), oldSnapshot); +}); + +test('T04-H66 an unreadable index witness cannot authorize index replacement', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H66_BASELINE', payload: { order: 1 } }); + const paths = getStateEnginePaths(rootDir); + const indexWitness = fs.readFileSync(paths.index); + const ledgerWitness = fs.readFileSync(paths.events); + const originalRead = fs.readFileSync; + let denied = 0; + fs.readFileSync = function denyIndexRead(target, ...args) { + if (String(target) === paths.index) { + denied += 1; + const error = new Error('Synthetic index witness EIO'); + error.code = 'EIO'; + throw error; + } + return originalRead.call(this, target, ...args); + }; + try { + assert.throws(() => loadStateIndex(rootDir), /index|witness|EIO|inspect/i, + 'Read failure must not cause replacement of a potentially acknowledged index witness'); + } finally { + fs.readFileSync = originalRead; + } + assert.ok(denied > 0); + assert.deepEqual(fs.readFileSync(paths.index), indexWitness); + assert.deepEqual(fs.readFileSync(paths.events), ledgerWitness); +}); + +test('T04-H66 pending recovery must compare an older index witness to its exact proposed event prefix', (t) => { + const rootDir = tempProject(t); + const alternativeRoot = tempProject(t); + const at = '2026-10-31T00:00:00.000Z'; + for (const root of [rootDir, alternativeRoot]) { + appendMetadataEvent({ rootDir: root, eventType: 'H66_FIRST', payload: { order: 1 }, timestamp: at }); + } + appendMetadataEvent({ rootDir, eventType: 'H66_ACKNOWLEDGED', payload: { order: 2 }, timestamp: at }); + appendMetadataEvent({ rootDir: alternativeRoot, eventType: 'H66_CONFLICT', payload: { order: 99 }, timestamp: at }); + appendMetadataEvent({ rootDir: alternativeRoot, eventType: 'H66_THIRD', payload: { order: 3 }, timestamp: at }); + const paths = getStateEnginePaths(rootDir); + const ledger = fs.readFileSync(paths.events, 'utf8'); + const prefix = ledger.slice(0, ledger.indexOf('\n') + 1); + const alternativeLedger = fs.readFileSync(getStateEnginePaths(alternativeRoot).events, 'utf8'); + assert.ok(alternativeLedger.startsWith(prefix)); + const indexWitness = fs.readFileSync(paths.index); + fs.writeFileSync(paths.events, prefix); + fs.unlinkSync(paths.snapshot); + const pending = JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(prefix), + previousHash: 'sha256:' + createHash('sha256').update(prefix).digest('hex'), + suffix: alternativeLedger.slice(prefix.length), + }); + fs.writeFileSync(paths.pending, pending); + assert.throws(() => loadCanonicalEvents(rootDir), /index|witness|canonical|history|conflict|hash/i, + 'Later proposed events cannot hide a conflict at the earlier acknowledged witness sequence'); + assert.equal(fs.readFileSync(paths.events, 'utf8'), prefix); + assert.deepEqual(fs.readFileSync(paths.index), indexWitness); + assert.equal(fs.readFileSync(paths.pending, 'utf8'), pending); +}); + +test('T04-H66 older index witness with a matching canonical event prefix remains rebuildable', (t) => { + const rootDir = tempProject(t); + appendMetadataEvent({ rootDir, eventType: 'H66_FIRST', payload: { order: 1 } }); + const paths = getStateEnginePaths(rootDir); + const oldIndex = fs.readFileSync(paths.index); + appendMetadataEvent({ rootDir, eventType: 'H66_SECOND', payload: { order: 2 } }); + appendMetadataEvent({ rootDir, eventType: 'H66_THIRD', payload: { order: 3 } }); + fs.writeFileSync(paths.index, oldIndex); + assert.equal(loadStateIndex(rootDir).sourceSequence, 3); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From 7900495825a16107b330d3ead3358f7f55628701 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 21:03:49 +0200 Subject: [PATCH 152/177] fix(T04-H67): preserve read-only index verification without implicit snapshot rebuild --- memory.md | 2 ++ runtime/orchestration/state-engine-v2.mjs | 6 ++++- scripts/state-engine-v2.test.mjs | 29 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/memory.md b/memory.md index 3075d8b43..df05f0e95 100644 --- a/memory.md +++ b/memory.md @@ -289,3 +289,5 @@ Integrated through PR #54; Issue #51 is completed. - Next validated increment: **DKF120-T04 — State Engine V2**. T04 requires its own Development Contract before production implementation. - **T04-H66 source-bound witness hardening (31 October 2026; NOT ACCEPTED):** Exact PR #92 SHA `3316ff1259da0d6b537a9bcc70de9e6e41d39fc5` was found by genuine independent Codex review to allow a reader-triggered index rebuild to overwrite later acknowledged history, and pending recovery to ignore conflicting older witness hashes. Isolated draft diagnostic PR #148 test-only SHA `833680235862671ec7c9815a54ba6f1f5758f778` reproduced H66 failures on Ubuntu and Windows in RED CI `41226287003`. Narrow fix SHA `b9a13b6aa3f42fffb15371c906ef0383bd28fd38` passed complete Ubuntu and Windows validation in GREEN CI `41226384214`. Compare each derived witness hash with the canonical event at the witness's own sequence; check both witnesses before rebuilding an index; fail closed on unreadable evidence. Exact-source independent reviews, physical NTFS power-loss assurance and risk-3 ACCEPTED remain outstanding; no merge or release authorized. + +- **T04-H67 read-only index verification correction (31 October 2026; NOT ACCEPTED):** Diagnostic PR #150 at exact test-only SHA `b1f7a2648ed65baab4e3c4fd46e6a27dab452558` reproduced on both Ubuntu and Windows in RED CI `41231620079` that `loadStateIndex({ rebuildIfNeeded: false })` silently rebuilt a missing snapshot. Narrow source correction `11f7b12b0974e1535877cc7ed2e54368bc798a54` honors the caller's no-rebuild boundary, returns null without publishing derived state when the source snapshot is absent, and preserves explicit rebuild behavior. Both complete Ubuntu and Windows validations PASS in GREEN CI `41231751410`. The test/source changes are confined to `runtime/orchestration/state-engine-v2.mjs` and `scripts/state-engine-v2.test.mjs`. Three distinct exact-source independent role approvals, controlled physical NTFS power-loss evidence, persisted risk-3 ACCEPTED and release approval remain outstanding; no merge/release authorized. diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 60a767b19..7fb38a878 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -1481,7 +1481,11 @@ export function rebuildStateIndex(rootDir = process.cwd()) { } export function loadStateIndex(rootDir = process.cwd(), { rebuildIfNeeded = true } = {}) { - const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: true }) ?? emptySnapshot(); + // Honor the caller's read-only boundary: index verification must not + // silently materialize a missing snapshot when rebuilding is disabled. + const loadedSnapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded }); + if (!loadedSnapshot && !rebuildIfNeeded) return null; + const snapshot = loadedSnapshot ?? emptySnapshot(); const paths = statePaths(rootDir); if (!fs.existsSync(paths.index)) { return rebuildIfNeeded ? rebuildStateIndex(rootDir) : null; diff --git a/scripts/state-engine-v2.test.mjs b/scripts/state-engine-v2.test.mjs index 5c0336fca..27f899e81 100644 --- a/scripts/state-engine-v2.test.mjs +++ b/scripts/state-engine-v2.test.mjs @@ -640,3 +640,32 @@ test('T04-SEC-JSON-01 rejects non-lossless JSON state before canonical commit', label + ' must not be acknowledged or committed to the canonical ledger'); } }); + + +test('T04-H67 read-only index lookup must not publish a missing snapshot', (t) => { + const rootDir = tempProject(t, 'dk-v13-h67-index-readonly-'); + appendEntityState({ + rootDir, entityType: 'generic-state', entityId: 'h67', + state: { stateRevision: 1, value: 'acknowledged' }, + actorClass: 'regression', + }); + const paths = getStateEnginePaths(rootDir); + const ledgerBefore = fs.readFileSync(paths.events); + const indexBefore = fs.readFileSync(paths.index); + fs.unlinkSync(paths.snapshot); + + // rebuildIfNeeded:false is a read-only verification boundary. It must not + // silently regenerate a missing materialized snapshot or return an index + // whose source snapshot is absent. + assert.equal(loadStateIndex(rootDir, { rebuildIfNeeded: false }), null); + assert.equal(fs.existsSync(paths.snapshot), false, + 'read-only index lookup must not recreate the missing snapshot'); + assert.deepEqual(fs.readFileSync(paths.events), ledgerBefore, + 'canonical acknowledged ledger must remain untouched'); + assert.deepEqual(fs.readFileSync(paths.index), indexBefore, + 'existing index witness must remain untouched'); + + const recovered = loadStateIndex(rootDir, { rebuildIfNeeded: true }); + assert.equal(recovered.sourceSequence, 1); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From f912c3a6cc65052fbd3ba94a95d0eac2f3a10ef9 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 21:32:27 +0200 Subject: [PATCH 153/177] fix(v0.13 T04): H68 preserve pending/torn recovery evidence during read-only index verification --- memory.md | 2 + runtime/orchestration/state-engine-v2.mjs | 15 ++++- scripts/state-engine-v2.test.mjs | 76 +++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/memory.md b/memory.md index df05f0e95..a9334b29b 100644 --- a/memory.md +++ b/memory.md @@ -291,3 +291,5 @@ Integrated through PR #54; Issue #51 is completed. - **T04-H66 source-bound witness hardening (31 October 2026; NOT ACCEPTED):** Exact PR #92 SHA `3316ff1259da0d6b537a9bcc70de9e6e41d39fc5` was found by genuine independent Codex review to allow a reader-triggered index rebuild to overwrite later acknowledged history, and pending recovery to ignore conflicting older witness hashes. Isolated draft diagnostic PR #148 test-only SHA `833680235862671ec7c9815a54ba6f1f5758f778` reproduced H66 failures on Ubuntu and Windows in RED CI `41226287003`. Narrow fix SHA `b9a13b6aa3f42fffb15371c906ef0383bd28fd38` passed complete Ubuntu and Windows validation in GREEN CI `41226384214`. Compare each derived witness hash with the canonical event at the witness's own sequence; check both witnesses before rebuilding an index; fail closed on unreadable evidence. Exact-source independent reviews, physical NTFS power-loss assurance and risk-3 ACCEPTED remain outstanding; no merge or release authorized. - **T04-H67 read-only index verification correction (31 October 2026; NOT ACCEPTED):** Diagnostic PR #150 at exact test-only SHA `b1f7a2648ed65baab4e3c4fd46e6a27dab452558` reproduced on both Ubuntu and Windows in RED CI `41231620079` that `loadStateIndex({ rebuildIfNeeded: false })` silently rebuilt a missing snapshot. Narrow source correction `11f7b12b0974e1535877cc7ed2e54368bc798a54` honors the caller's no-rebuild boundary, returns null without publishing derived state when the source snapshot is absent, and preserves explicit rebuild behavior. Both complete Ubuntu and Windows validations PASS in GREEN CI `41231751410`. The test/source changes are confined to `runtime/orchestration/state-engine-v2.mjs` and `scripts/state-engine-v2.test.mjs`. Three distinct exact-source independent role approvals, controlled physical NTFS power-loss evidence, persisted risk-3 ACCEPTED and release approval remain outstanding; no merge/release authorized. + +- **T04-H68 read-only recovery evidence preservation (31 October 2026; NOT ACCEPTED):** Exact PR #92 base SHA `7900495825a16107b330d3ead3358f7f55628701` could mutate `pending-commit.json`, missing derived snapshot and unwitnessed torn ledger suffix during `loadStateIndex({ rebuildIfNeeded: false })`. Test-only draft PR #154 SHA `2c2f9206b7d7ea0ff8eeff2d7720e5c65a89f228` reproduced both regressions on Ubuntu and Windows in RED CI `41234074315`. Narrow correction in draft PR #153 SHA `4d48e64a0594a587ad61fd3693ae065db30fadfb` makes explicit no-rebuild canonical reads fail closed on pending recovery or unterminated ledger tails, preserving crash evidence, while retaining the H17 explicit integrity-recovery contract. Complete Ubuntu and Windows validation PASS in GREEN CI `41234104614`; source blob `65c2681eac2b7a890c9825beba0b76f9de0b13df`, test blob `c266d928ba1514a8d5018ce2ef19798f2259d1ae`. Integrated into PR #92 only after this cross-platform proof. H68 legacy migration/cutover P1, Issue #73 controlled physical NTFS power-loss evidence, three distinct authenticated exact-source reviews and persisted risk-3 ACCEPTED remain outstanding. No merge or release authorized. diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 7fb38a878..65c2681ea 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -1089,21 +1089,30 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { return prefixEvents; } -export function loadCanonicalEvents(rootDir = process.cwd()) { +export function loadCanonicalEvents(rootDir = process.cwd(), { recoverIfNeeded = true } = {}) { const paths = statePaths(rootDir); if (!fs.existsSync(paths.events)) { assertMissingCanonicalLedgerHasNoStateWitness(paths); return []; } if (fs.existsSync(paths.pending)) { + if (!recoverIfNeeded) { + throw new StateEngineError('Pending State Engine commit requires recovery; read-only lookup cannot retire recovery evidence'); + } return withStateLock(rootDir, () => parseLedger(fs.readFileSync(paths.events, 'utf8'))); } const content = fs.readFileSync(paths.events, 'utf8'); if (fs.existsSync(paths.pending)) { + if (!recoverIfNeeded) { + throw new StateEngineError('Pending State Engine commit requires recovery; read-only lookup cannot retire recovery evidence'); + } return withStateLock(rootDir, () => parseLedger(fs.readFileSync(paths.events, 'utf8'))); } if (!content || content.endsWith('\n')) return parseLedger(content); + if (!recoverIfNeeded) { + throw new StateEngineError('Canonical ledger has an unterminated tail requiring recovery; read-only lookup cannot repair it'); + } // Only a non-terminated final record is eligible for automatic tail // recovery. Mid-ledger corruption and hash-chain failures remain fail-closed. @@ -1410,7 +1419,7 @@ export function loadStateSnapshot(rootDir = process.cwd(), { rebuildIfNeeded = t assertMissingCanonicalLedgerHasNoStateWitness(paths); return null; } - const canonical = rebuildSnapshotFromEvents(loadCanonicalEvents(rootDir)); + const canonical = rebuildSnapshotFromEvents(loadCanonicalEvents(rootDir, { recoverIfNeeded: rebuildIfNeeded })); if (!fs.existsSync(paths.snapshot)) { if (!rebuildIfNeeded) return null; @@ -2033,6 +2042,8 @@ export function queryStateIndex({ } export function verifyStateEngineIntegrity(rootDir = process.cwd()) { + // Preserve the established H17 integrity-check contract: this explicit + // recovery entry point may complete a pending commit before verification. const events = loadCanonicalEvents(rootDir); const canonical = rebuildSnapshotFromEvents(events); const snapshot = loadStateSnapshot(rootDir, { rebuildIfNeeded: false }); diff --git a/scripts/state-engine-v2.test.mjs b/scripts/state-engine-v2.test.mjs index 27f899e81..c266d928b 100644 --- a/scripts/state-engine-v2.test.mjs +++ b/scripts/state-engine-v2.test.mjs @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; +import crypto from 'node:crypto'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; @@ -669,3 +670,78 @@ test('T04-H67 read-only index lookup must not publish a missing snapshot', (t) = assert.equal(recovered.sourceSequence, 1); assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); }); + +test('T04-H68 read-only index verification preserves a valid pending journal and missing snapshot', (t) => { + const rootDir = tempProject(t, 'dk-v13-h68-readonly-pending-'); + const paths = getStateEnginePaths(rootDir); + appendEntityState({ + rootDir, entityType: 'generic-state', entityId: 'h68', + state: { stateRevision: 1, value: 'first' }, actorClass: 'regression', + }); + const acknowledgedPrefix = fs.readFileSync(paths.events); + const originalIndex = fs.readFileSync(paths.index); + appendEntityState({ + rootDir, entityType: 'generic-state', entityId: 'h68', + state: { stateRevision: 2, value: 'second' }, actorClass: 'regression', + }); + const completeLedger = fs.readFileSync(paths.events); + const pendingSuffix = completeLedger.subarray(acknowledgedPrefix.length).toString('utf8'); + assert.ok(pendingSuffix.endsWith('\n')); + fs.writeFileSync(paths.events, acknowledgedPrefix); + fs.writeFileSync(paths.index, originalIndex); + fs.unlinkSync(paths.snapshot); + const pending = { + schemaVersion: '1.0.0', + previousByteLength: acknowledgedPrefix.length, + previousHash: 'sha256:' + crypto.createHash('sha256').update(acknowledgedPrefix).digest('hex'), + suffix: pendingSuffix, + }; + fs.writeFileSync(paths.pending, JSON.stringify(pending) + '\n'); + const journalBefore = fs.readFileSync(paths.pending); + + assert.throws( + () => loadStateIndex(rootDir, { rebuildIfNeeded: false }), + /pending|recovery|read.only/i, + 'read-only verification must refuse to recover an interrupted commit', + ); + assert.deepEqual(fs.readFileSync(paths.events), acknowledgedPrefix, + 'read-only verification must not append pending suffix'); + assert.deepEqual(fs.readFileSync(paths.pending), journalBefore, + 'read-only verification must not retire recovery journal'); + assert.equal(fs.existsSync(paths.snapshot), false, + 'read-only verification must not materialize missing snapshot'); + assert.deepEqual(fs.readFileSync(paths.index), originalIndex, + 'read-only verification must not replace index witness'); + + const recovered = loadStateIndex(rootDir, { rebuildIfNeeded: true }); + assert.equal(recovered.sourceSequence, 2); + assert.equal(fs.existsSync(paths.pending), false); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); + +test('T04-H68 read-only index verification never truncates an unacknowledged torn tail', (t) => { + const rootDir = tempProject(t, 'dk-v13-h68-readonly-tail-'); + appendEntityState({ + rootDir, entityType: 'generic-state', entityId: 'h68-tail', + state: { stateRevision: 1, value: 'acknowledged' }, actorClass: 'regression', + }); + const paths = getStateEnginePaths(rootDir); + fs.appendFileSync(paths.events, '{"partial":'); + const ledgerBefore = fs.readFileSync(paths.events); + const snapshotBefore = fs.readFileSync(paths.snapshot); + const indexBefore = fs.readFileSync(paths.index); + + assert.throws( + () => loadStateIndex(rootDir, { rebuildIfNeeded: false }), + /tail|repair|read.only|recovery/i, + 'read-only verification must refuse canonical tail repair', + ); + assert.deepEqual(fs.readFileSync(paths.events), ledgerBefore, + 'read-only verification must preserve the torn tail for recovery'); + assert.deepEqual(fs.readFileSync(paths.snapshot), snapshotBefore); + assert.deepEqual(fs.readFileSync(paths.index), indexBefore); + + const repaired = loadStateIndex(rootDir, { rebuildIfNeeded: true }); + assert.equal(repaired.sourceSequence, 1); + assert.equal(verifyStateEngineIntegrity(rootDir).valid, true); +}); From 91564ad0bbc55cf306858512b95f3e846e2f0ad3 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 22:56:12 +0200 Subject: [PATCH 154/177] fix(v0.13 T04): H68 serialize legacy writers with migration cutover --- memory.md | 2 + runtime/autopilot/state-store.mjs | 7 +- runtime/orchestration/orchestration-run.mjs | 16 ++- .../orchestration/state-engine-migration.mjs | 11 +- .../orchestration/state-migration-gate.mjs | 109 ++++++++++++++++++ scripts/state-engine-v2-migration.test.mjs | 82 +++++++++++++ 6 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 runtime/orchestration/state-migration-gate.mjs diff --git a/memory.md b/memory.md index a9334b29b..b2417d529 100644 --- a/memory.md +++ b/memory.md @@ -293,3 +293,5 @@ Integrated through PR #54; Issue #51 is completed. - **T04-H67 read-only index verification correction (31 October 2026; NOT ACCEPTED):** Diagnostic PR #150 at exact test-only SHA `b1f7a2648ed65baab4e3c4fd46e6a27dab452558` reproduced on both Ubuntu and Windows in RED CI `41231620079` that `loadStateIndex({ rebuildIfNeeded: false })` silently rebuilt a missing snapshot. Narrow source correction `11f7b12b0974e1535877cc7ed2e54368bc798a54` honors the caller's no-rebuild boundary, returns null without publishing derived state when the source snapshot is absent, and preserves explicit rebuild behavior. Both complete Ubuntu and Windows validations PASS in GREEN CI `41231751410`. The test/source changes are confined to `runtime/orchestration/state-engine-v2.mjs` and `scripts/state-engine-v2.test.mjs`. Three distinct exact-source independent role approvals, controlled physical NTFS power-loss evidence, persisted risk-3 ACCEPTED and release approval remain outstanding; no merge/release authorized. - **T04-H68 read-only recovery evidence preservation (31 October 2026; NOT ACCEPTED):** Exact PR #92 base SHA `7900495825a16107b330d3ead3358f7f55628701` could mutate `pending-commit.json`, missing derived snapshot and unwitnessed torn ledger suffix during `loadStateIndex({ rebuildIfNeeded: false })`. Test-only draft PR #154 SHA `2c2f9206b7d7ea0ff8eeff2d7720e5c65a89f228` reproduced both regressions on Ubuntu and Windows in RED CI `41234074315`. Narrow correction in draft PR #153 SHA `4d48e64a0594a587ad61fd3693ae065db30fadfb` makes explicit no-rebuild canonical reads fail closed on pending recovery or unterminated ledger tails, preserving crash evidence, while retaining the H17 explicit integrity-recovery contract. Complete Ubuntu and Windows validation PASS in GREEN CI `41234104614`; source blob `65c2681eac2b7a890c9825beba0b76f9de0b13df`, test blob `c266d928ba1514a8d5018ce2ef19798f2259d1ae`. Integrated into PR #92 only after this cross-platform proof. H68 legacy migration/cutover P1, Issue #73 controlled physical NTFS power-loss evidence, three distinct authenticated exact-source reviews and persisted risk-3 ACCEPTED remain outstanding. No merge or release authorized. + +- **T04-H68 legacy migration/cutover writer serialization (31 October 2026; NOT ACCEPTED):** Genuine PR #92 P1 at source `7900495825a16107b330d3ead3358f7f55628701` demonstrated an acknowledged legacy writer after migration inspection could be omitted from the backup/V2 while MIGRATION_COMPLETED falsely reported equivalence. Test-only diagnostic PR #152 SHA `1c887b6fac7657b69d98491d4aee833582eb162e` reproduced the failure in RED CI `41233222609`. Dedicated fail-closed cross-process gate now spans inspection, backup, V2 import and completion, and the supported Autopilot/orchestration state writers acquire it before selecting legacy or V2 authority. New tests verify interleaved writer rejection/retry, independent-process exclusion and orphaned-gate evidence preservation. Exact diagnostic source SHA `b589d9cd2aaf09cf3226fe2ba967a7dd492c32ed` passed complete Ubuntu and Windows validation in GREEN CI `41240549095`. An orphaned gate is never automatically reclaimed; offline operator recovery requires stopping all participating writers, preserving gate/legacy/V2 evidence and proving exclusive ownership before manual removal. This cooperative gate does not claim protection against arbitrary same-UID attackers (Issue #72). Issue #73 physical NTFS power-loss evidence, independent exact-source role approvals, two complete final integration-head matrices and persisted risk-3 ACCEPTED remain outstanding. No merge/release authorization. diff --git a/runtime/autopilot/state-store.mjs b/runtime/autopilot/state-store.mjs index 03c4e0753..7f463f4c5 100644 --- a/runtime/autopilot/state-store.mjs +++ b/runtime/autopilot/state-store.mjs @@ -14,6 +14,7 @@ import path from 'node:path'; import { validateWorkflowState } from './validators.mjs'; import { acquireTransactionLock, releaseTransactionLock } from './lock-manager.mjs'; +import { withMigrationGate } from '../orchestration/state-migration-gate.mjs'; import { appendEntityState, hasCompletedStateMigration, @@ -120,7 +121,7 @@ function saveLegacyStateRevision(state, rootDir) { } } -export function saveStateRevision(state, rootDir = process.cwd()) { +function saveStateRevisionUnlocked(state, rootDir = process.cwd()) { validateWorkflowState(state); if (!shouldUseV2(rootDir)) { @@ -138,6 +139,10 @@ export function saveStateRevision(state, rootDir = process.cwd()) { return state; } +export function saveStateRevision(state, rootDir = process.cwd()) { + return withMigrationGate(rootDir, () => saveStateRevisionUnlocked(state, rootDir)); +} + function recoverLatestLegacyState(rootDir) { const stateDir = getStateDir(rootDir); if (!fs.existsSync(stateDir)) return null; diff --git a/runtime/orchestration/orchestration-run.mjs b/runtime/orchestration/orchestration-run.mjs index 316db5a6a..c71007b07 100644 --- a/runtime/orchestration/orchestration-run.mjs +++ b/runtime/orchestration/orchestration-run.mjs @@ -5,6 +5,7 @@ import { validateDevelopmentContract } from './development-contract.mjs'; import { resolveContractGateProfiles } from './gate-profiles.mjs'; import { selectExecutionStrategy } from './host-capabilities.mjs'; import { getRunDirectory } from './evidence-store.mjs'; +import { withMigrationGate } from './state-migration-gate.mjs'; import { appendEntityState, canonicalStateJson, @@ -174,14 +175,14 @@ export function validateOrchestrationRun(run) { return true; } -export function persistRunManifest(run, rootDir = process.cwd()) { +function persistRunManifestUnlocked(run, rootDir = process.cwd()) { validateOrchestrationRun(run); if (run.stateRevision !== 1) throw new OrchestrationRunError('Initial run manifest may only persist stateRevision 1'); const runDir = getRunDirectory(rootDir, run.contractId, run.runId); return persistImmutable(path.join(runDir, 'manifest.json'), run, 'orchestration run manifest'); } -export function persistRunStateRevision(run, rootDir = process.cwd()) { +function persistRunStateRevisionUnlocked(run, rootDir = process.cwd()) { validateOrchestrationRun(run); if (!shouldUseV2RunState(run.contractId, run.runId, rootDir)) { @@ -236,13 +237,22 @@ export function persistRunStateRevision(run, rootDir = process.cwd()) { }; } -export function persistFinalRunState(run, rootDir = process.cwd()) { +function persistFinalRunStateUnlocked(run, rootDir = process.cwd()) { validateOrchestrationRun(run); if (!['ACCEPTED', 'BLOCKED'].includes(run.state)) throw new OrchestrationRunError('Final run state requires ACCEPTED or BLOCKED'); const runDir = getRunDirectory(rootDir, run.contractId, run.runId); return persistImmutable(path.join(runDir, 'final-state.json'), run, 'final run state'); } +export function persistRunManifest(run, rootDir = process.cwd()) { + return withMigrationGate(rootDir, () => persistRunManifestUnlocked(run, rootDir)); +} +export function persistRunStateRevision(run, rootDir = process.cwd()) { + return withMigrationGate(rootDir, () => persistRunStateRevisionUnlocked(run, rootDir)); +} +export function persistFinalRunState(run, rootDir = process.cwd()) { + return withMigrationGate(rootDir, () => persistFinalRunStateUnlocked(run, rootDir)); +} export function loadRunManifest(contractId, runId, rootDir = process.cwd()) { const filePath = path.join(getRunDirectory(rootDir, contractId, runId), 'manifest.json'); if (!fs.existsSync(filePath)) return null; diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index ac3e030e1..3682256d7 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -5,6 +5,7 @@ import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { validateWorkflowState } from '../autopilot/validators.mjs'; +import { withMigrationGate } from './state-migration-gate.mjs'; import { validateOrchestrationRun } from './orchestration-run.mjs'; import { appendEntityState, @@ -458,7 +459,7 @@ function verifySemanticEquivalence(histories, rootDir) { }; } -export function migrateLegacyStateToV2({ +function migrateLegacyStateToV2Unlocked({ rootDir = process.cwd(), completedAt = new Date().toISOString(), } = {}) { @@ -537,6 +538,14 @@ export function migrateLegacyStateToV2({ }); } +// The gate must span inspection, backup publication, all canonical imports, +// equivalence verification and MIGRATION_COMPLETED publication. A writer must +// take the same gate *before* choosing legacy or V2 authority. +export function migrateLegacyStateToV2(options = {}) { + const rootDir = options.rootDir ?? process.cwd(); + return withMigrationGate(rootDir, () => migrateLegacyStateToV2Unlocked(options)); +} + function resolveSafeRestoreDestination(targetRoot, relativePath, { createMissing = false } = {}) { const root = path.resolve(targetRoot); const destination = resolveProjectPath(root, relativePath); diff --git a/runtime/orchestration/state-migration-gate.mjs b/runtime/orchestration/state-migration-gate.mjs new file mode 100644 index 000000000..57132f882 --- /dev/null +++ b/runtime/orchestration/state-migration-gate.mjs @@ -0,0 +1,109 @@ +/** + * Serializes migration inspection/backup/cutover with every supported legacy + * state writer. This is a local, cooperative cross-process gate, NOT a defence + * against arbitrary same-account hostile processes (Issue #72). + * + * An orphaned gate is deliberately NOT reclaimed by elapsed time: an + * uncertain writer or migration must never be bypassed. Offline recovery + * requires proving all participating processes have stopped and preserving + * the lock bytes before an operator removes it. + */ +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +const SLEEP = new Int32Array(new SharedArrayBuffer(4)); +const LOCK_NAME = '.state-migration.lock'; + +export class MigrationGateError extends Error { + constructor(message) { + super(message); + this.name = 'MigrationGateError'; + } +} + +function assertSafeDirectory(directory) { + const stat = fs.lstatSync(directory); + if (!stat.isDirectory() || stat.isSymbolicLink()) { + throw new MigrationGateError('Migration gate parent is not a real directory'); + } +} + +function inspectGate(lockPath) { + try { + const stat = fs.lstatSync(lockPath, { bigint: true }); + if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n) { + throw new MigrationGateError('Unsafe migration gate link or object type'); + } + return stat; + } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } +} + +export function withMigrationGate(rootDir, operation, { timeoutMs = 2000 } = {}) { + const root = path.resolve(rootDir); + assertSafeDirectory(root); + const parent = path.join(root, '.development-kit'); + fs.mkdirSync(parent, { recursive: true }); + assertSafeDirectory(parent); + const lockPath = path.join(parent, LOCK_NAME); + const token = crypto.randomUUID(); + const start = Date.now(); + let fd; + for (;;) { + try { + fd = fs.openSync(lockPath, 'wx', 0o600); + break; + } catch (error) { + if (error.code !== 'EEXIST') throw error; + inspectGate(lockPath); + if (Date.now() - start >= timeoutMs) { + throw new MigrationGateError( + 'Migration/cutover gate is held or orphaned; preserve it and retry only after safe operator recovery', + ); + } + Atomics.wait(SLEEP, 0, 0, 25); + } + } + let identity; + try { + identity = fs.fstatSync(fd, { bigint: true }); + fs.writeFileSync(fd, JSON.stringify({ + schemaVersion: 1, token, pid: process.pid, acquiredAt: new Date().toISOString(), + }) + '\n', 'utf8'); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + let outcome; + let operationError; + try { + outcome = operation(); + } catch (error) { + operationError = error; + } + let releaseError; + try { + assertSafeDirectory(parent); + const current = inspectGate(lockPath); + if (!current || current.dev !== identity.dev || current.ino !== identity.ino) { + throw new MigrationGateError('Migration gate identity changed; preserving evidence'); + } + const record = JSON.parse(fs.readFileSync(lockPath, 'utf8')); + if (record.token !== token || record.pid !== process.pid) { + throw new MigrationGateError('Migration gate ownership changed; preserving evidence'); + } + fs.unlinkSync(lockPath); + } catch (error) { + releaseError = error; + } + if (operationError && releaseError) { + throw new AggregateError([operationError, releaseError], + 'Migration operation and gate cleanup both failed; outcome uncertain'); + } + if (operationError) throw operationError; + if (releaseError) throw releaseError; + return outcome; +} diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index aaff7a6ed..1b87597d8 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -28,6 +28,7 @@ import { import { loadCurrentRunState, } from '../runtime/orchestration/orchestration-run.mjs'; +import { withMigrationGate } from '../runtime/orchestration/state-migration-gate.mjs'; function tempProject(t, prefix = 'dk-state-migration-') { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); @@ -1189,3 +1190,84 @@ test('W73-28 Unicode uppercase-folded restore aliases fail closed before publica assert.equal(fs.existsSync(path.join(targetRoot, relativeDir, 's.json')), false, 'Rejected backup must not publish the staged candidate'); }); + + +test('T04-H68 migration serializes a competing Autopilot writer across cutover', (t) => { + const rootDir = tempProject(t, 'dk-v13-h68-legacy-cutover-'); + const legacy = writeLegacyAutopilot(rootDir, 1); + const backupPath = getLegacyBackupPath(rootDir); + const originalRename = fs.renameSync; + let injected = false; + try { + fs.renameSync = function (from, to) { + const result = originalRename.call(fs, from, to); + if (!injected && path.resolve(to) === path.resolve(backupPath)) { + injected = true; + // A writer racing migration must not acknowledge an unbacked legacy + // revision. The migration owns the gate before inspection; a nested + // competing writer fails closed, then may retry after cutover. + assert.throws( + () => saveStateRevision(workflowState(2, legacy.workflowId), rootDir), + /migration|cutover|gate|orphaned/i, + ); + } + return result; + }; + const migrated = migrateLegacyStateToV2({ + rootDir, completedAt: '2026-10-31T19:00:00.000Z', + }); + assert.equal(migrated.semanticEquivalence, true); + } finally { + fs.renameSync = originalRename; + } + + assert.equal(injected, true); + assert.equal(fs.existsSync(path.join(legacy.stateDir, 'revision-000002.json')), false, + 'a competing writer cannot publish an unbacked legacy revision'); + assert.equal(getCurrentState(rootDir).stateRevision, 1); + assert.equal(loadCanonicalEvents(rootDir).filter((event) => + event.eventType === 'MIGRATION_COMPLETED').length, 1); + + // The rejected writer can retry after cutover and must append to V2. + saveStateRevision(workflowState(2, legacy.workflowId), rootDir); + assert.equal(getCurrentState(rootDir).stateRevision, 2); + assert.equal(loadEntityState('autopilot-workflow', legacy.workflowId, rootDir).stateRevision, 2); + assert.equal(fs.existsSync(path.join(legacy.stateDir, 'revision-000002.json')), false); +}); + +test('T04-H68 orphaned migration gate fails closed without reclaiming a journal or original tree', (t) => { + const rootDir = tempProject(t, 'dk-v13-h68-orphan-'); + const legacy = writeLegacyAutopilot(rootDir, 1); + const gatePath = path.join(rootDir, '.development-kit', '.state-migration.lock'); + const witness = '{"owner":"unknown"}\\n'; + fs.writeFileSync(gatePath, witness, { flag: 'wx' }); + assert.throws(() => migrateLegacyStateToV2({ rootDir }), /migration|gate|orphaned/i); + assert.equal(fs.readFileSync(gatePath, 'utf8'), witness); + assert.equal(getCurrentState(rootDir).stateRevision, 1); + assert.equal(fs.existsSync(getLegacyBackupPath(rootDir)), false); + assert.equal(fs.existsSync(path.join(legacy.stateDir, 'revision-000001.json')), true); +}); + +test('T04-H68 separate process cannot acknowledge legacy state while migration gate is owned', (t) => { + const rootDir = tempProject(t, 'dk-v13-h68-cross-process-'); + const legacy = writeLegacyAutopilot(rootDir, 1); + const state = workflowState(2, legacy.workflowId); + const childScript = [ + 'import { saveStateRevision } from ' + + JSON.stringify(new URL('../runtime/autopilot/state-store.mjs', import.meta.url).href) + ';', + 'const state = JSON.parse(process.argv[2]);', + 'saveStateRevision(state, process.argv[1]);', + ].join('\n'); + withMigrationGate(rootDir, () => { + const child = requireSpawnSync(process.execPath, + ['--input-type=module', '-e', childScript, rootDir, JSON.stringify(state)], + { encoding: 'utf8', timeout: 15_000 }); + assert.notEqual(child.status, 0, 'separate writer must not acknowledge during cutover'); + assert.match(child.stderr, /MigrationGateError|migration.*gate|orphaned/i); + assert.equal(getCurrentState(rootDir).stateRevision, 1); + assert.equal(fs.existsSync(path.join(legacy.stateDir, 'revision-000002.json')), false); + }); + // A separate-process denial must not poison the gate after safe release. + saveStateRevision(state, rootDir); + assert.equal(getCurrentState(rootDir).stateRevision, 2); +}); From c9fac8fafecdb4ca730ce57c22be0974a5714d4a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 23:22:23 +0200 Subject: [PATCH 155/177] fix(t04): H69 gate pointer recovery and H70 fail closed on legacy I/O faults --- memory.md | 2 + runtime/autopilot/state-store.mjs | 70 ++++++++----- scripts/state-engine-v2-migration.test.mjs | 108 +++++++++++++++++++++ 3 files changed, 156 insertions(+), 24 deletions(-) diff --git a/memory.md b/memory.md index b2417d529..db086b89a 100644 --- a/memory.md +++ b/memory.md @@ -295,3 +295,5 @@ Integrated through PR #54; Issue #51 is completed. - **T04-H68 read-only recovery evidence preservation (31 October 2026; NOT ACCEPTED):** Exact PR #92 base SHA `7900495825a16107b330d3ead3358f7f55628701` could mutate `pending-commit.json`, missing derived snapshot and unwitnessed torn ledger suffix during `loadStateIndex({ rebuildIfNeeded: false })`. Test-only draft PR #154 SHA `2c2f9206b7d7ea0ff8eeff2d7720e5c65a89f228` reproduced both regressions on Ubuntu and Windows in RED CI `41234074315`. Narrow correction in draft PR #153 SHA `4d48e64a0594a587ad61fd3693ae065db30fadfb` makes explicit no-rebuild canonical reads fail closed on pending recovery or unterminated ledger tails, preserving crash evidence, while retaining the H17 explicit integrity-recovery contract. Complete Ubuntu and Windows validation PASS in GREEN CI `41234104614`; source blob `65c2681eac2b7a890c9825beba0b76f9de0b13df`, test blob `c266d928ba1514a8d5018ce2ef19798f2259d1ae`. Integrated into PR #92 only after this cross-platform proof. H68 legacy migration/cutover P1, Issue #73 controlled physical NTFS power-loss evidence, three distinct authenticated exact-source reviews and persisted risk-3 ACCEPTED remain outstanding. No merge or release authorized. - **T04-H68 legacy migration/cutover writer serialization (31 October 2026; NOT ACCEPTED):** Genuine PR #92 P1 at source `7900495825a16107b330d3ead3358f7f55628701` demonstrated an acknowledged legacy writer after migration inspection could be omitted from the backup/V2 while MIGRATION_COMPLETED falsely reported equivalence. Test-only diagnostic PR #152 SHA `1c887b6fac7657b69d98491d4aee833582eb162e` reproduced the failure in RED CI `41233222609`. Dedicated fail-closed cross-process gate now spans inspection, backup, V2 import and completion, and the supported Autopilot/orchestration state writers acquire it before selecting legacy or V2 authority. New tests verify interleaved writer rejection/retry, independent-process exclusion and orphaned-gate evidence preservation. Exact diagnostic source SHA `b589d9cd2aaf09cf3226fe2ba967a7dd492c32ed` passed complete Ubuntu and Windows validation in GREEN CI `41240549095`. An orphaned gate is never automatically reclaimed; offline operator recovery requires stopping all participating writers, preserving gate/legacy/V2 evidence and proving exclusive ownership before manual removal. This cooperative gate does not claim protection against arbitrary same-UID attackers (Issue #72). Issue #73 physical NTFS power-loss evidence, independent exact-source role approvals, two complete final integration-head matrices and persisted risk-3 ACCEPTED remain outstanding. No merge/release authorization. + +- **T04-H69/H70 legacy pointer recovery and I/O fail-closed hardening (31 October 2026; NOT ACCEPTED):** Independent PR #92 P2 `#discussion_r4433270346` showed explicit and automatic legacy pointer recovery could mutate legacy authority during migration without the H68 cross-process gate. Test-only diagnostic PR #156 SHA `429fe1d24b07ab7aaf7aaf79c1a1f787122e5832d789` reproduced both bypasses in RED CI `41241772321`; narrowly scoped source SHA `5d14a709b45537e75f26308ab50a27638edca291` passed complete Windows and Ubuntu validation (Ubuntu UI-preview health-check flake passed on retry) in CI `41241884533`. Further source audit identified H70: transient ENOSPC publishing the newest valid legacy pointer or EIO reading the newest acknowledged revision could be swallowed and silently roll back to an older revision. Diagnostic PR #157 test-only SHA `f6da53258cb3762efddefe4194a80858dc1dae5e` reproduced both defects on Ubuntu and Windows in RED CI `41242533566`. Corrected diagnostic SHA `df52faf7e18fcbc0065bcd8bcd7e793a89f943fe` passed **both complete Ubuntu and Windows** validation jobs in GREEN CI `41242573501`; only invalid revision content is eligible for fallback, while read/write I/O faults propagate fail-closed. Ported only byte-identical validated source blob `8c6662d597accb152b7be30c521b09a6233a12e9` and test blob `be14055c64e0388de56709c7e2c63543dd2ea255` plus this memory evidence to authoritative v0.13 PR #92. Fresh exact-head integration CI, two independent full final-head Ubuntu/Windows executions, exact-source independent code/architecture/security approvals, controlled physical NTFS sudden-power-loss evidence under Issue #73 and persisted risk-3 ACCEPTED remain mandatory. No merge or release authorized. diff --git a/runtime/autopilot/state-store.mjs b/runtime/autopilot/state-store.mjs index 7f463f4c5..8c6662d59 100644 --- a/runtime/autopilot/state-store.mjs +++ b/runtime/autopilot/state-store.mjs @@ -143,7 +143,7 @@ export function saveStateRevision(state, rootDir = process.cwd()) { return withMigrationGate(rootDir, () => saveStateRevisionUnlocked(state, rootDir)); } -function recoverLatestLegacyState(rootDir) { +function recoverLatestLegacyStateUnlocked(rootDir) { const stateDir = getStateDir(rootDir); if (!fs.existsSync(stateDir)) return null; @@ -153,38 +153,60 @@ function recoverLatestLegacyState(rootDir) { .reverse(); for (const file of files) { + // Only malformed revision content is eligible for fallback. An I/O error + // while reading the newest revision is not evidence that it is invalid: + // silently choosing an older one would roll back acknowledged history. + const filePath = path.join(stateDir, file); + const content = fs.readFileSync(filePath, 'utf8'); + let state; try { - const filePath = path.join(stateDir, file); - const state = JSON.parse(fs.readFileSync(filePath, 'utf8')); + state = JSON.parse(content); validateWorkflowState(state); - - const pointerContent = JSON.stringify({ - currentRevision: state.stateRevision, - currentRevisionFile: file, - workflowId: state.workflowId, - updatedAt: new Date().toISOString(), - recoveredAt: new Date().toISOString(), - }, null, 2); - - fs.writeFileSync(path.join(stateDir, 'current.json'), pointerContent, 'utf8'); - return state; } catch { - // Try previous revision. + // Corrupt revision content may be skipped in favor of an older valid one. + continue; } + + const pointerContent = JSON.stringify({ + currentRevision: state.stateRevision, + currentRevisionFile: file, + workflowId: state.workflowId, + updatedAt: new Date().toISOString(), + recoveredAt: new Date().toISOString(), + }, null, 2); + + // Publication errors must propagate; never retry with an older revision + // after the latest valid revision has already been selected. + fs.writeFileSync(path.join(stateDir, 'current.json'), pointerContent, 'utf8'); + return state; } return null; } +// Pointer repair is a legacy write. Reacquire the migration gate and recheck +// authority *inside* it so a stale reader cannot repair legacy after cutover. +function recoverLatestLegacyState(rootDir) { + return withMigrationGate(rootDir, () => { + if (shouldUseV2(rootDir) && isStateEngineV2Active(rootDir)) { + const current = migratedWorkflowState(rootDir); + if (current) return current; + } + return recoverLatestLegacyStateUnlocked(rootDir); + }); +} + export function recoverLatestValidState(rootDir = process.cwd()) { - if (shouldUseV2(rootDir) && isStateEngineV2Active(rootDir)) { - const current = migratedWorkflowState(rootDir); - if (current) { - rebuildStateSnapshot(rootDir); - const rebuilt = loadLatestEntityState('autopilot-workflow', rootDir); - validateWorkflowState(rebuilt); - return rebuilt; + return withMigrationGate(rootDir, () => { + if (shouldUseV2(rootDir) && isStateEngineV2Active(rootDir)) { + const current = migratedWorkflowState(rootDir); + if (current) { + rebuildStateSnapshot(rootDir); + const rebuilt = loadLatestEntityState('autopilot-workflow', rootDir); + validateWorkflowState(rebuilt); + return rebuilt; + } } - } - return recoverLatestLegacyState(rootDir); + return recoverLatestLegacyStateUnlocked(rootDir); + }); } diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index 1b87597d8..be14055c6 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -23,6 +23,7 @@ import { } from '../runtime/orchestration/state-engine-migration.mjs'; import { getCurrentState, + recoverLatestValidState, saveStateRevision, } from '../runtime/autopilot/state-store.mjs'; import { @@ -1271,3 +1272,110 @@ test('T04-H68 separate process cannot acknowledge legacy state while migration g saveStateRevision(state, rootDir); assert.equal(getCurrentState(rootDir).stateRevision, 2); }); + +test('T04-H69 explicit legacy recovery cannot rewrite pointer during migration cutover', (t) => { + const rootDir = tempProject(t, 'dk-v13-h69-explicit-recovery-'); + const legacy = writeLegacyAutopilot(rootDir, 1); + const pointerPath = path.join(legacy.stateDir, 'current.json'); + const originalPointer = fs.readFileSync(pointerPath); + const backupPath = getLegacyBackupPath(rootDir); + const originalRename = fs.renameSync; + let injected = false; + try { + fs.renameSync = function (from, to) { + const result = originalRename.call(fs, from, to); + if (!injected && path.resolve(to) === path.resolve(backupPath)) { + injected = true; + assert.throws(() => recoverLatestValidState(rootDir), + /migration|cutover|gate|orphaned/i); + assert.deepEqual(fs.readFileSync(pointerPath), originalPointer, + 'recovery must not modify acknowledged legacy pointer during cutover'); + } + return result; + }; + const migrated = migrateLegacyStateToV2({ rootDir }); + assert.equal(migrated.semanticEquivalence, true); + } finally { + fs.renameSync = originalRename; + } + assert.equal(injected, true); + assert.deepEqual(fs.readFileSync(pointerPath), originalPointer); + assert.equal(getCurrentState(rootDir).stateRevision, 1); +}); + +test('T04-H69 automatic pointer recovery cannot bypass held migration gate', (t) => { + const rootDir = tempProject(t, 'dk-v13-h69-automatic-recovery-'); + const legacy = writeLegacyAutopilot(rootDir, 1); + const pointerPath = path.join(legacy.stateDir, 'current.json'); + const broken = '{"corrupted":true}\\n'; + fs.writeFileSync(pointerPath, broken); + withMigrationGate(rootDir, () => { + assert.throws(() => getCurrentState(rootDir), /migration|cutover|gate|orphaned/i); + assert.equal(fs.readFileSync(pointerPath, 'utf8'), broken, + 'automatic recovery must not rewrite pointer while another migration owns the gate'); + }); + assert.equal(getCurrentState(rootDir).stateRevision, 1, + 'after safe gate release, normal recovery may repair the legacy pointer'); +}); + + +test('T04-H70 transient pointer write failure cannot silently roll legacy recovery back', (t) => { + const rootDir = tempProject(t, 'dk-v13-h70-pointer-write-'); + const legacy = writeLegacyAutopilot(rootDir, 2); + const pointerPath = path.join(legacy.stateDir, 'current.json'); + const broken = '{"corrupted":true}\n'; + fs.writeFileSync(pointerPath, broken); + const originalWrite = fs.writeFileSync; + let injected = 0; + fs.writeFileSync = function (target, content, ...args) { + if (typeof target === 'string' && path.resolve(target) === path.resolve(pointerPath) + && injected++ === 0) { + const error = new Error('injected ENOSPC writing recovery pointer'); + error.code = 'ENOSPC'; + throw error; + } + return originalWrite.call(this, target, content, ...args); + }; + try { + assert.throws(() => recoverLatestValidState(rootDir), /ENOSPC|injected/i, + 'a failed pointer publication must propagate, not retry with an older revision'); + } finally { + fs.writeFileSync = originalWrite; + } + assert.equal(injected, 1); + assert.equal(fs.readFileSync(pointerPath, 'utf8'), broken, + 'a failed pointer repair must leave the original witness unchanged'); + assert.equal(fs.existsSync(path.join(legacy.stateDir, 'revision-000002.json')), true); + assert.equal(getCurrentState(rootDir).stateRevision, 2, + 'after I/O is restored the latest acknowledged revision remains recoverable'); +}); + +test('T04-H70 transient newest revision read failure cannot select older revision', (t) => { + const rootDir = tempProject(t, 'dk-v13-h70-revision-read-'); + const legacy = writeLegacyAutopilot(rootDir, 2); + const pointerPath = path.join(legacy.stateDir, 'current.json'); + const broken = '{"corrupted":true}\n'; + fs.writeFileSync(pointerPath, broken); + const newest = path.join(legacy.stateDir, 'revision-000002.json'); + const originalRead = fs.readFileSync; + let injected = 0; + fs.readFileSync = function (target, ...args) { + if (typeof target === 'string' && path.resolve(target) === path.resolve(newest) + && injected++ === 0) { + const error = new Error('injected EIO reading newest acknowledged revision'); + error.code = 'EIO'; + throw error; + } + return originalRead.call(this, target, ...args); + }; + try { + assert.throws(() => recoverLatestValidState(rootDir), /EIO|injected/i, + 'transient I/O failure must not be mistaken for an invalid revision'); + } finally { + fs.readFileSync = originalRead; + } + assert.equal(injected, 1); + assert.equal(fs.readFileSync(pointerPath, 'utf8'), broken, + 'a failed read must not publish an older pointer'); + assert.equal(getCurrentState(rootDir).stateRevision, 2); +}); From 7a3b8a23b3f59ef4c3a7346f0ebc6e9cd5d05981 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sat, 31 Oct 2026 23:23:19 +0200 Subject: [PATCH 156/177] docs(t04): correct exact H69 RED test commit provenance --- memory.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memory.md b/memory.md index db086b89a..ac22a7279 100644 --- a/memory.md +++ b/memory.md @@ -296,4 +296,4 @@ Integrated through PR #54; Issue #51 is completed. - **T04-H68 legacy migration/cutover writer serialization (31 October 2026; NOT ACCEPTED):** Genuine PR #92 P1 at source `7900495825a16107b330d3ead3358f7f55628701` demonstrated an acknowledged legacy writer after migration inspection could be omitted from the backup/V2 while MIGRATION_COMPLETED falsely reported equivalence. Test-only diagnostic PR #152 SHA `1c887b6fac7657b69d98491d4aee833582eb162e` reproduced the failure in RED CI `41233222609`. Dedicated fail-closed cross-process gate now spans inspection, backup, V2 import and completion, and the supported Autopilot/orchestration state writers acquire it before selecting legacy or V2 authority. New tests verify interleaved writer rejection/retry, independent-process exclusion and orphaned-gate evidence preservation. Exact diagnostic source SHA `b589d9cd2aaf09cf3226fe2ba967a7dd492c32ed` passed complete Ubuntu and Windows validation in GREEN CI `41240549095`. An orphaned gate is never automatically reclaimed; offline operator recovery requires stopping all participating writers, preserving gate/legacy/V2 evidence and proving exclusive ownership before manual removal. This cooperative gate does not claim protection against arbitrary same-UID attackers (Issue #72). Issue #73 physical NTFS power-loss evidence, independent exact-source role approvals, two complete final integration-head matrices and persisted risk-3 ACCEPTED remain outstanding. No merge/release authorization. -- **T04-H69/H70 legacy pointer recovery and I/O fail-closed hardening (31 October 2026; NOT ACCEPTED):** Independent PR #92 P2 `#discussion_r4433270346` showed explicit and automatic legacy pointer recovery could mutate legacy authority during migration without the H68 cross-process gate. Test-only diagnostic PR #156 SHA `429fe1d24b07ab7aaf7aaf79c1a1f787122e5832d789` reproduced both bypasses in RED CI `41241772321`; narrowly scoped source SHA `5d14a709b45537e75f26308ab50a27638edca291` passed complete Windows and Ubuntu validation (Ubuntu UI-preview health-check flake passed on retry) in CI `41241884533`. Further source audit identified H70: transient ENOSPC publishing the newest valid legacy pointer or EIO reading the newest acknowledged revision could be swallowed and silently roll back to an older revision. Diagnostic PR #157 test-only SHA `f6da53258cb3762efddefe4194a80858dc1dae5e` reproduced both defects on Ubuntu and Windows in RED CI `41242533566`. Corrected diagnostic SHA `df52faf7e18fcbc0065bcd8bcd7e793a89f943fe` passed **both complete Ubuntu and Windows** validation jobs in GREEN CI `41242573501`; only invalid revision content is eligible for fallback, while read/write I/O faults propagate fail-closed. Ported only byte-identical validated source blob `8c6662d597accb152b7be30c521b09a6233a12e9` and test blob `be14055c64e0388de56709c7e2c63543dd2ea255` plus this memory evidence to authoritative v0.13 PR #92. Fresh exact-head integration CI, two independent full final-head Ubuntu/Windows executions, exact-source independent code/architecture/security approvals, controlled physical NTFS sudden-power-loss evidence under Issue #73 and persisted risk-3 ACCEPTED remain mandatory. No merge or release authorized. +- **T04-H69/H70 legacy pointer recovery and I/O fail-closed hardening (31 October 2026; NOT ACCEPTED):** Independent PR #92 P2 `#discussion_r4433270346` showed explicit and automatic legacy pointer recovery could mutate legacy authority during migration without the H68 cross-process gate. Test-only diagnostic PR #156 SHA `429fe1d24b07ab7aaf79c1a1f787122e5832d789` reproduced both bypasses in RED CI `41241772321`; narrowly scoped source SHA `5d14a709b45537e75f26308ab50a27638edca291` passed complete Windows and Ubuntu validation (Ubuntu UI-preview health-check flake passed on retry) in CI `41241884533`. Further source audit identified H70: transient ENOSPC publishing the newest valid legacy pointer or EIO reading the newest acknowledged revision could be swallowed and silently roll back to an older revision. Diagnostic PR #157 test-only SHA `f6da53258cb3762efddefe4194a80858dc1dae5e` reproduced both defects on Ubuntu and Windows in RED CI `41242533566`. Corrected diagnostic SHA `df52faf7e18fcbc0065bcd8bcd7e793a89f943fe` passed **both complete Ubuntu and Windows** validation jobs in GREEN CI `41242573501`; only invalid revision content is eligible for fallback, while read/write I/O faults propagate fail-closed. Ported only byte-identical validated source blob `8c6662d597accb152b7be30c521b09a6233a12e9` and test blob `be14055c64e0388de56709c7e2c63543dd2ea255` plus this memory evidence to authoritative v0.13 PR #92. Fresh exact-head integration CI, two independent full final-head Ubuntu/Windows executions, exact-source independent code/architecture/security approvals, controlled physical NTFS sudden-power-loss evidence under Issue #73 and persisted risk-3 ACCEPTED remain mandatory. No merge or release authorized. From edd8f5b3f7d0428f73eca7a2e0b821fbcf991254 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 00:04:01 +0200 Subject: [PATCH 157/177] fix(t04-h71): propagate legacy pointer and revision read I/O --- runtime/autopilot/state-store.mjs | 43 ++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/runtime/autopilot/state-store.mjs b/runtime/autopilot/state-store.mjs index 8c6662d59..a38e85bd8 100644 --- a/runtime/autopilot/state-store.mjs +++ b/runtime/autopilot/state-store.mjs @@ -54,21 +54,44 @@ function getLegacyCurrentState(rootDir) { const stateDir = getStateDir(rootDir); const currentFile = path.join(stateDir, 'current.json'); - if (!fs.existsSync(currentFile)) return null; + // Only an actually absent pointer may return null. Read failures such as + // EIO/EACCES must not be treated as malformed content and trigger a write. + let pointerContent; + try { + pointerContent = fs.readFileSync(currentFile, 'utf8'); + } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } + let pointer; try { - const pointer = JSON.parse(fs.readFileSync(currentFile, 'utf8')); - const revFile = path.join(stateDir, pointer.currentRevisionFile); - if (fs.existsSync(revFile)) { - const state = JSON.parse(fs.readFileSync(revFile, 'utf8')); - validateWorkflowState(state); - return state; - } + pointer = JSON.parse(pointerContent); } catch { - // Pointer or state file corrupt — attempt legacy recovery. + // Syntactically invalid pointer content may be recovered under the gate. + return recoverLatestLegacyState(rootDir); + } + if (!pointer || typeof pointer.currentRevisionFile !== 'string' + || !/^revision-\d{6}\.json$/.test(pointer.currentRevisionFile)) { + return recoverLatestLegacyState(rootDir); } - return recoverLatestLegacyState(rootDir); + const revFile = path.join(stateDir, pointer.currentRevisionFile); + let revisionContent; + try { + revisionContent = fs.readFileSync(revFile, 'utf8'); + } catch (error) { + if (error.code === 'ENOENT') return recoverLatestLegacyState(rootDir); + throw error; + } + try { + const state = JSON.parse(revisionContent); + validateWorkflowState(state); + return state; + } catch { + // Only malformed revision content/validation may select recovery. + return recoverLatestLegacyState(rootDir); + } } export function getCurrentState(rootDir = process.cwd()) { From 6bb41133094d2110735674534fd16f0599241fe3 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 00:04:09 +0200 Subject: [PATCH 158/177] test(t04-h71): preserve legacy pointer on transient read failures --- scripts/state-engine-v2-migration.test.mjs | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index be14055c6..8e5fc76b4 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -1379,3 +1379,61 @@ test('T04-H70 transient newest revision read failure cannot select older revisio 'a failed read must not publish an older pointer'); assert.equal(getCurrentState(rootDir).stateRevision, 2); }); + + +// H71 negative controls: a transient I/O failure is not malformed JSON. +test('T04-H71 legacy current pointer EIO fails closed without rewriting acknowledged pointer', (t) => { + const rootDir = tempProject(t, 'dk-v13-h71-pointer-eio-'); + const legacy = writeLegacyAutopilot(rootDir, 2); + const pointerPath = path.join(legacy.stateDir, 'current.json'); + const originalPointer = fs.readFileSync(pointerPath); + const originalRead = fs.readFileSync; + let injected = 0; + fs.readFileSync = function (target, ...args) { + if (typeof target === 'string' && path.resolve(target) === path.resolve(pointerPath) + && injected++ === 0) { + const error = new Error('injected EIO reading acknowledged legacy pointer'); + error.code = 'EIO'; + throw error; + } + return originalRead.call(this, target, ...args); + }; + try { + assert.throws(() => getCurrentState(rootDir), /EIO|injected/i, + 'pointer read I/O error is not evidence of corrupt JSON and must not trigger repair'); + } finally { + fs.readFileSync = originalRead; + } + assert.equal(injected, 1); + assert.deepEqual(fs.readFileSync(pointerPath), originalPointer, + 'an uncertain pointer read must preserve the original acknowledged pointer'); + assert.equal(getCurrentState(rootDir).stateRevision, 2); +}); + +test('T04-H71 legacy current revision EIO fails closed without rewriting pointer', (t) => { + const rootDir = tempProject(t, 'dk-v13-h71-revision-eio-'); + const legacy = writeLegacyAutopilot(rootDir, 2); + const pointerPath = path.join(legacy.stateDir, 'current.json'); + const latestPath = path.join(legacy.stateDir, 'revision-000002.json'); + const originalPointer = fs.readFileSync(pointerPath); + const originalRead = fs.readFileSync; + let injected = 0; + fs.readFileSync = function (target, ...args) { + if (typeof target === 'string' && path.resolve(target) === path.resolve(latestPath) + && injected++ === 0) { + const error = new Error('injected EIO reading current acknowledged revision'); + error.code = 'EIO'; + throw error; + } + return originalRead.call(this, target, ...args); + }; + try { + assert.throws(() => getCurrentState(rootDir), /EIO|injected/i, + 'revision read I/O error must propagate rather than invoking pointer repair'); + } finally { + fs.readFileSync = originalRead; + } + assert.equal(injected, 1); + assert.deepEqual(fs.readFileSync(pointerPath), originalPointer); + assert.equal(getCurrentState(rootDir).stateRevision, 2); +}); From 4f50559ce8cf8c125212c7cea3999bae02859352 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 00:04:20 +0200 Subject: [PATCH 159/177] docs(t04): record H71 test-first RED/GREEN and release blockers --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index ac22a7279..6d5be9187 100644 --- a/memory.md +++ b/memory.md @@ -297,3 +297,5 @@ Integrated through PR #54; Issue #51 is completed. - **T04-H68 legacy migration/cutover writer serialization (31 October 2026; NOT ACCEPTED):** Genuine PR #92 P1 at source `7900495825a16107b330d3ead3358f7f55628701` demonstrated an acknowledged legacy writer after migration inspection could be omitted from the backup/V2 while MIGRATION_COMPLETED falsely reported equivalence. Test-only diagnostic PR #152 SHA `1c887b6fac7657b69d98491d4aee833582eb162e` reproduced the failure in RED CI `41233222609`. Dedicated fail-closed cross-process gate now spans inspection, backup, V2 import and completion, and the supported Autopilot/orchestration state writers acquire it before selecting legacy or V2 authority. New tests verify interleaved writer rejection/retry, independent-process exclusion and orphaned-gate evidence preservation. Exact diagnostic source SHA `b589d9cd2aaf09cf3226fe2ba967a7dd492c32ed` passed complete Ubuntu and Windows validation in GREEN CI `41240549095`. An orphaned gate is never automatically reclaimed; offline operator recovery requires stopping all participating writers, preserving gate/legacy/V2 evidence and proving exclusive ownership before manual removal. This cooperative gate does not claim protection against arbitrary same-UID attackers (Issue #72). Issue #73 physical NTFS power-loss evidence, independent exact-source role approvals, two complete final integration-head matrices and persisted risk-3 ACCEPTED remain outstanding. No merge/release authorization. - **T04-H69/H70 legacy pointer recovery and I/O fail-closed hardening (31 October 2026; NOT ACCEPTED):** Independent PR #92 P2 `#discussion_r4433270346` showed explicit and automatic legacy pointer recovery could mutate legacy authority during migration without the H68 cross-process gate. Test-only diagnostic PR #156 SHA `429fe1d24b07ab7aaf79c1a1f787122e5832d789` reproduced both bypasses in RED CI `41241772321`; narrowly scoped source SHA `5d14a709b45537e75f26308ab50a27638edca291` passed complete Windows and Ubuntu validation (Ubuntu UI-preview health-check flake passed on retry) in CI `41241884533`. Further source audit identified H70: transient ENOSPC publishing the newest valid legacy pointer or EIO reading the newest acknowledged revision could be swallowed and silently roll back to an older revision. Diagnostic PR #157 test-only SHA `f6da53258cb3762efddefe4194a80858dc1dae5e` reproduced both defects on Ubuntu and Windows in RED CI `41242533566`. Corrected diagnostic SHA `df52faf7e18fcbc0065bcd8bcd7e793a89f943fe` passed **both complete Ubuntu and Windows** validation jobs in GREEN CI `41242573501`; only invalid revision content is eligible for fallback, while read/write I/O faults propagate fail-closed. Ported only byte-identical validated source blob `8c6662d597accb152b7be30c521b09a6233a12e9` and test blob `be14055c64e0388de56709c7e2c63543dd2ea255` plus this memory evidence to authoritative v0.13 PR #92. Fresh exact-head integration CI, two independent full final-head Ubuntu/Windows executions, exact-source independent code/architecture/security approvals, controlled physical NTFS sudden-power-loss evidence under Issue #73 and persisted risk-3 ACCEPTED remain mandatory. No merge or release authorized. + +- **T04-H71 legacy pointer/revision read-I/O fail-closed correction (31 October 2026; NOT ACCEPTED):** `getLegacyCurrentState()` previously caught EIO while reading acknowledged `current.json` or its current revision as if it were malformed JSON, then entered recovery and rewrote the pointer. Test-only draft diagnostic PR #159 SHA `e7dc2570264c43eb5cb98b562717345634efdaac` reproduced both failures on Ubuntu and Windows in RED CI `41245623327` (`not ok 126/127`, missing expected I/O exception). Narrow correction diagnostic SHA `8dfba42f40103635da3725f075da6d1593bc92bd` separates filesystem read errors from malformed pointer/revision content: ENOENT retains the existing absent/missing-revision recovery semantics, EIO/EACCES propagate without mutation, and only canonical `revision-NNNNNN.json` pointer targets are accepted. Both complete Ubuntu and Windows validations passed in GREEN CI `41245703767`, including both H71 tests and v0.13 compatibility. Ported byte-identical runtime source blob `a38e85bd8c0fbb008f8ef28c4f7e9f05dc6de0bb` and test blob `8e5fc76b42079790a5209c2f9da7186eec0c176d` into authoritative draft PR #92. Prior H69 review thread `#discussion_r4433270346` was resolved with source-bound RED/GREEN evidence, not counted as an independent role approval. Fresh final integration-head two-run Ubuntu/Windows validation, three distinct authenticated exact-source code/architecture/security reviews, physical NTFS sudden-power-loss assurance (Issue #73) and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge or release authorization. From 2f08f1f7cd685c9043f9110f72a3e5cfbdea431a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 00:29:23 +0200 Subject: [PATCH 160/177] fix(t04): reject symlinked legacy migration inputs before backup (H72 GREEN) --- .../orchestration/state-engine-migration.mjs | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index 3682256d7..d6c5d93f1 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -91,13 +91,37 @@ function atomicWrite(filePath, content) { fs.renameSync(temp, filePath); } -function listFilesRecursive(directory) { - if (!fs.existsSync(directory)) return []; +function listFilesRecursive(directory, rootDir) { + // A legacy input consumed through a symlink cannot be silently omitted + // from the immutable backup manifest/fingerprint. Check every ancestor, + // including a junctioned state root, before inspecting its descendants. + let ancestor = path.resolve(rootDir); + for (const part of projectRelative(rootDir, directory).split('/')) { + ancestor = path.join(ancestor, part); + let metadata; + try { + metadata = fs.lstatSync(ancestor); + } catch (error) { + if (error.code === 'ENOENT') return []; + throw error; + } + if (metadata.isSymbolicLink()) { + throw new StateMigrationError( + 'Legacy backup source contains symbolic link: ' + projectRelative(rootDir, ancestor), + ); + } + } + const files = []; function walk(current) { const entries = fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)); for (const entry of entries) { const full = path.join(current, entry.name); + if (entry.isSymbolicLink()) { + throw new StateMigrationError( + 'Legacy backup source contains symbolic link: ' + projectRelative(rootDir, full), + ); + } if (entry.isDirectory()) walk(full); else if (entry.isFile()) files.push(full); } @@ -109,13 +133,13 @@ function listFilesRecursive(directory) { function collectLegacyBackupFiles(rootDir) { const files = new Set(); const autopilotState = path.join(rootDir, '.development-kit', 'autopilot', 'state'); - for (const file of listFilesRecursive(autopilotState)) { + for (const file of listFilesRecursive(autopilotState, rootDir)) { const name = path.basename(file); if (name === 'current.json' || /^revision-\d{6}\.json$/.test(name)) files.add(file); } const runsRoot = path.join(rootDir, '.development-kit', 'runs'); - for (const file of listFilesRecursive(runsRoot)) { + for (const file of listFilesRecursive(runsRoot, rootDir)) { const name = path.basename(file); const parent = path.basename(path.dirname(file)); if ( From 08c2be5a3cd56f2cdbdb1ef54e863b67118389be Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 00:29:26 +0200 Subject: [PATCH 161/177] test(t04): preserve H72 RED/GREEN symlink and junction regressions --- scripts/state-engine-v2-migration.test.mjs | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index 8e5fc76b4..174ea4917 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -1437,3 +1437,71 @@ test('T04-H71 legacy current revision EIO fails closed without rewriting pointer assert.deepEqual(fs.readFileSync(pointerPath), originalPointer); assert.equal(getCurrentState(rootDir).stateRevision, 2); }); + + +test('T04-H72 symlinked legacy Autopilot revision cannot be omitted from backup or certified', (t) => { + const rootDir = tempProject(t, 'dk-v13-h72-revision-link-'); + const legacy = writeLegacyAutopilot(rootDir, 1); + const revision = path.join(legacy.stateDir, 'revision-000001.json'); + const external = path.join(rootDir, 'external-revision.json'); + fs.renameSync(revision, external); + try { + fs.symlinkSync(external, revision, 'file'); + } catch (error) { + if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { + t.skip('Host does not permit file symlink creation'); + return; + } + throw error; + } + const before = fs.readFileSync(external); + assert.throws(() => inspectLegacyState(rootDir), /symbolic link|symlink/i, + 'inspection must not certify a backup that omits an imported revision'); + assert.throws(() => migrateLegacyStateToV2({ rootDir }), /symbolic link|symlink/i); + assert.deepEqual(fs.readFileSync(external), before, 'original revision must remain unchanged'); + assert.equal(fs.readlinkSync(revision), external, 'original link must remain intact'); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false, + 'rejected migration must not publish a V2 ledger or partial backup'); +}); + +test('T04-H72 symlinked legacy Autopilot pointer cannot be omitted from backup', (t) => { + const rootDir = tempProject(t, 'dk-v13-h72-pointer-link-'); + const legacy = writeLegacyAutopilot(rootDir, 1); + const pointer = path.join(legacy.stateDir, 'current.json'); + const external = path.join(rootDir, 'external-current.json'); + fs.renameSync(pointer, external); + try { + fs.symlinkSync(external, pointer, 'file'); + } catch (error) { + if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { + t.skip('Host does not permit file symlink creation'); + return; + } + throw error; + } + assert.throws(() => inspectLegacyState(rootDir), /symbolic link|symlink/i); + assert.throws(() => migrateLegacyStateToV2({ rootDir }), /symbolic link|symlink/i); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false); +}); + + +test('T04-H72 junctioned legacy state root cannot publish a fingerprint for external inputs', (t) => { + const rootDir = tempProject(t, 'dk-v13-h72-state-junction-'); + const legacy = writeLegacyAutopilot(rootDir, 1); + const external = path.join(rootDir, 'external-legacy-state'); + fs.renameSync(legacy.stateDir, external); + try { + fs.symlinkSync(external, legacy.stateDir, process.platform === 'win32' ? 'junction' : 'dir'); + } catch (error) { + if (['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) { + t.skip('Host does not permit directory junction/symlink creation'); + return; + } + throw error; + } + assert.throws(() => inspectLegacyState(rootDir), /symbolic link|symlink/i); + assert.throws(() => migrateLegacyStateToV2({ rootDir }), /symbolic link|symlink/i); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false); + assert.equal(fs.existsSync(path.join(external, 'revision-000001.json')), true, + 'rejected migration must retain original revision bytes'); +}); From e9ba6e3bfcf8d859401f5501e3a1d77178dbcd34 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 00:29:35 +0200 Subject: [PATCH 162/177] docs(t04): record H72 test-first RED/GREEN source and Windows junction evidence --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index 6d5be9187..351ae4ec0 100644 --- a/memory.md +++ b/memory.md @@ -299,3 +299,5 @@ Integrated through PR #54; Issue #51 is completed. - **T04-H69/H70 legacy pointer recovery and I/O fail-closed hardening (31 October 2026; NOT ACCEPTED):** Independent PR #92 P2 `#discussion_r4433270346` showed explicit and automatic legacy pointer recovery could mutate legacy authority during migration without the H68 cross-process gate. Test-only diagnostic PR #156 SHA `429fe1d24b07ab7aaf79c1a1f787122e5832d789` reproduced both bypasses in RED CI `41241772321`; narrowly scoped source SHA `5d14a709b45537e75f26308ab50a27638edca291` passed complete Windows and Ubuntu validation (Ubuntu UI-preview health-check flake passed on retry) in CI `41241884533`. Further source audit identified H70: transient ENOSPC publishing the newest valid legacy pointer or EIO reading the newest acknowledged revision could be swallowed and silently roll back to an older revision. Diagnostic PR #157 test-only SHA `f6da53258cb3762efddefe4194a80858dc1dae5e` reproduced both defects on Ubuntu and Windows in RED CI `41242533566`. Corrected diagnostic SHA `df52faf7e18fcbc0065bcd8bcd7e793a89f943fe` passed **both complete Ubuntu and Windows** validation jobs in GREEN CI `41242573501`; only invalid revision content is eligible for fallback, while read/write I/O faults propagate fail-closed. Ported only byte-identical validated source blob `8c6662d597accb152b7be30c521b09a6233a12e9` and test blob `be14055c64e0388de56709c7e2c63543dd2ea255` plus this memory evidence to authoritative v0.13 PR #92. Fresh exact-head integration CI, two independent full final-head Ubuntu/Windows executions, exact-source independent code/architecture/security approvals, controlled physical NTFS sudden-power-loss evidence under Issue #73 and persisted risk-3 ACCEPTED remain mandatory. No merge or release authorized. - **T04-H71 legacy pointer/revision read-I/O fail-closed correction (31 October 2026; NOT ACCEPTED):** `getLegacyCurrentState()` previously caught EIO while reading acknowledged `current.json` or its current revision as if it were malformed JSON, then entered recovery and rewrote the pointer. Test-only draft diagnostic PR #159 SHA `e7dc2570264c43eb5cb98b562717345634efdaac` reproduced both failures on Ubuntu and Windows in RED CI `41245623327` (`not ok 126/127`, missing expected I/O exception). Narrow correction diagnostic SHA `8dfba42f40103635da3725f075da6d1593bc92bd` separates filesystem read errors from malformed pointer/revision content: ENOENT retains the existing absent/missing-revision recovery semantics, EIO/EACCES propagate without mutation, and only canonical `revision-NNNNNN.json` pointer targets are accepted. Both complete Ubuntu and Windows validations passed in GREEN CI `41245703767`, including both H71 tests and v0.13 compatibility. Ported byte-identical runtime source blob `a38e85bd8c0fbb008f8ef28c4f7e9f05dc6de0bb` and test blob `8e5fc76b42079790a5209c2f9da7186eec0c176d` into authoritative draft PR #92. Prior H69 review thread `#discussion_r4433270346` was resolved with source-bound RED/GREEN evidence, not counted as an independent role approval. Fresh final integration-head two-run Ubuntu/Windows validation, three distinct authenticated exact-source code/architecture/security reviews, physical NTFS sudden-power-loss assurance (Issue #73) and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge or release authorization. + +- **T04-H72 legacy symlink/backup completeness (1 November 2026; NOT ACCEPTED):** Independent exact-source PR #92 review at `4f50559ce8cf8c125212c7cea3999bae02859352` found that `collectLegacyBackupFiles()` silently omitted symlinked Autopilot revision/pointer inputs while `autopilotHistory()` followed and imported them, allowing false semantic equivalence, incomplete backup and incomplete source fingerprint. Test-only diagnostic draft PR #162 commit `52b9b3b6f8fb38cbfa70d67a34e2e58c3836fd9d` reproduced both defects on **Ubuntu and Windows** in RED CI `41247622308` (both full validation jobs failed on the expected H72 negative controls). Narrow fail-closed fix rejects symlinks and junctions in scanned legacy source trees and their ancestor paths before backup/fingerprint/cutover. Three H72 regressions (symlinked revision, symlinked pointer and junctioned state root) all passed without skips on both platforms in full GREEN CI `41247716349` on diagnostic source `64ae5a15f82ed58805da79ae0887ceff9e93bfcc`; earlier GREEN `41247698757` also passed both full platforms for the first two tests. Ported only validated runtime blob `d6c5d93f117146db5b24c5372562c0c04c767329` and test blob `174ea49179691686a1d72be9cae463ccbcf728a1` byte-identically into authoritative v0.13 draft PR #92 (runtime commit `2f08f1f7cd685c9043f9110f72a3e5cfbdea431a`, tests commit `08c2be5a3cd56f2cdbdb1ef54e863b67118389be`). This prevents false backup certification for symlinked source inputs; it does not prove hostile same-UID TOCTOU safety, NTFS sudden physical power-loss durability, three independent exact-final-source role approvals or persisted deterministic risk-3 ACCEPTED. Fresh final integration-head dual Ubuntu/Windows validation is required; no merge/release authority. From bd7bebad9368b81674789cb707dae72bdc388863 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 01:16:19 +0200 Subject: [PATCH 163/177] fix(T04-H73): preserve orphaned legacy witnesses during v0.13 migration --- memory.md | 2 ++ .../orchestration/state-engine-migration.mjs | 18 ++++++++-- scripts/state-engine-v2-migration.test.mjs | 35 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/memory.md b/memory.md index 351ae4ec0..a3982a3fa 100644 --- a/memory.md +++ b/memory.md @@ -301,3 +301,5 @@ Integrated through PR #54; Issue #51 is completed. - **T04-H71 legacy pointer/revision read-I/O fail-closed correction (31 October 2026; NOT ACCEPTED):** `getLegacyCurrentState()` previously caught EIO while reading acknowledged `current.json` or its current revision as if it were malformed JSON, then entered recovery and rewrote the pointer. Test-only draft diagnostic PR #159 SHA `e7dc2570264c43eb5cb98b562717345634efdaac` reproduced both failures on Ubuntu and Windows in RED CI `41245623327` (`not ok 126/127`, missing expected I/O exception). Narrow correction diagnostic SHA `8dfba42f40103635da3725f075da6d1593bc92bd` separates filesystem read errors from malformed pointer/revision content: ENOENT retains the existing absent/missing-revision recovery semantics, EIO/EACCES propagate without mutation, and only canonical `revision-NNNNNN.json` pointer targets are accepted. Both complete Ubuntu and Windows validations passed in GREEN CI `41245703767`, including both H71 tests and v0.13 compatibility. Ported byte-identical runtime source blob `a38e85bd8c0fbb008f8ef28c4f7e9f05dc6de0bb` and test blob `8e5fc76b42079790a5209c2f9da7186eec0c176d` into authoritative draft PR #92. Prior H69 review thread `#discussion_r4433270346` was resolved with source-bound RED/GREEN evidence, not counted as an independent role approval. Fresh final integration-head two-run Ubuntu/Windows validation, three distinct authenticated exact-source code/architecture/security reviews, physical NTFS sudden-power-loss assurance (Issue #73) and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge or release authorization. - **T04-H72 legacy symlink/backup completeness (1 November 2026; NOT ACCEPTED):** Independent exact-source PR #92 review at `4f50559ce8cf8c125212c7cea3999bae02859352` found that `collectLegacyBackupFiles()` silently omitted symlinked Autopilot revision/pointer inputs while `autopilotHistory()` followed and imported them, allowing false semantic equivalence, incomplete backup and incomplete source fingerprint. Test-only diagnostic draft PR #162 commit `52b9b3b6f8fb38cbfa70d67a34e2e58c3836fd9d` reproduced both defects on **Ubuntu and Windows** in RED CI `41247622308` (both full validation jobs failed on the expected H72 negative controls). Narrow fail-closed fix rejects symlinks and junctions in scanned legacy source trees and their ancestor paths before backup/fingerprint/cutover. Three H72 regressions (symlinked revision, symlinked pointer and junctioned state root) all passed without skips on both platforms in full GREEN CI `41247716349` on diagnostic source `64ae5a15f82ed58805da79ae0887ceff9e93bfcc`; earlier GREEN `41247698757` also passed both full platforms for the first two tests. Ported only validated runtime blob `d6c5d93f117146db5b24c5372562c0c04c767329` and test blob `174ea49179691686a1d72be9cae463ccbcf728a1` byte-identically into authoritative v0.13 draft PR #92 (runtime commit `2f08f1f7cd685c9043f9110f72a3e5cfbdea431a`, tests commit `08c2be5a3cd56f2cdbdb1ef54e863b67118389be`). This prevents false backup certification for symlinked source inputs; it does not prove hostile same-UID TOCTOU safety, NTFS sudden physical power-loss durability, three independent exact-final-source role approvals or persisted deterministic risk-3 ACCEPTED. Fresh final integration-head dual Ubuntu/Windows validation is required; no merge/release authority. + +- **T04-H73 orphaned legacy witness fail-closed correction (1 November 2026; NOT ACCEPTED):** Exact v0.13 PR #92 source `e9ba6e3bfcf8d859401f5501e3a1d77178dbcd34` silently treated a surviving Autopilot `current.json` without revision files and orchestration `state-revisions`/pointer without immutable `manifest.json` as empty histories. This could certify an incomplete migration and publish `MIGRATION_COMPLETED` while legacy evidence remained. Diagnostic draft PR #164 committed two adversarial tests first at `5e6f57d1b3b34cb32182987a694eeff9caf1c2a1`; both reproduced as expected failures on Ubuntu and Windows in RED CI `41251264165`. Narrow source fix rejects both incomplete legacy witness classes before canonical layout/backup/cutover; the original legacy files remain intact. Complete Ubuntu and Windows validation passed in GREEN CI `41251304713` on diagnostic commit `6d3228d7c25753b1747cebe5d6ad3220c6b22731`. Runtime source blob `7f46e190a1dcb4dfab8c8c6d81c663edca33bb69` and test blob `7cf5b15d9b6259096992c422bd0f2740c4442c2d` are the exact validated artifacts. Three independent authenticated exact-final-source role approvals, two complete final-head platform runs, physical NTFS sudden-power-loss assurance under Issue #73 and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge or release authority. diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index d6c5d93f1..7f46e190a 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -235,7 +235,12 @@ function autopilotHistory(rootDir) { .filter((name) => /^revision-\d{6}\.json$/.test(name)) .sort(); - if (revisionFiles.length === 0) return null; + if (revisionFiles.length === 0) { + if (fs.existsSync(path.join(stateDir, 'current.json'))) { + throw new StateMigrationError('Legacy Autopilot current pointer exists without revision history'); + } + return null; + } const currentPath = path.join(stateDir, 'current.json'); if (!fs.existsSync(currentPath)) throw new StateMigrationError('Legacy Autopilot current.json is missing'); @@ -309,7 +314,16 @@ function orchestrationHistories(rootDir) { for (const runEntry of runDirs) { const runDir = path.join(contractDir, runEntry.name); const manifestPath = path.join(runDir, 'manifest.json'); - if (!fs.existsSync(manifestPath)) continue; + if (!fs.existsSync(manifestPath)) { + const revisionDir = path.join(runDir, 'state-revisions'); + const orphanRevisions = fs.existsSync(revisionDir) + && fs.readdirSync(revisionDir).some((name) => /^\d{8}\.json$/.test(name)); + if (orphanRevisions || fs.existsSync(path.join(runDir, 'current-state.json')) + || fs.existsSync(path.join(runDir, 'final-state.json'))) { + throw new StateMigrationError('Legacy orchestration manifest is missing for existing run history'); + } + continue; + } const manifest = readJson(manifestPath, 'Legacy orchestration manifest'); try { validateOrchestrationRun(manifest); } catch (error) { throw new StateMigrationError(`Legacy orchestration manifest is invalid: ${error.message}`); diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index 174ea4917..7cf5b15d9 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -1505,3 +1505,38 @@ test('T04-H72 junctioned legacy state root cannot publish a fingerprint for exte assert.equal(fs.existsSync(path.join(external, 'revision-000001.json')), true, 'rejected migration must retain original revision bytes'); }); + + +test('T04-H73 orphaned Autopilot pointer cannot be silently certified as empty legacy history', (t) => { + const rootDir = tempProject(t, 'dk-v13-h73-orphan-autopilot-'); + const legacy = writeLegacyAutopilot(rootDir, 1); + const pointer = path.join(legacy.stateDir, 'current.json'); + const original = fs.readFileSync(pointer); + fs.unlinkSync(path.join(legacy.stateDir, 'revision-000001.json')); + + assert.throws(() => inspectLegacyState(rootDir), + /Autopilot|revision|orphan|incomplete|missing/i, + 'a surviving current pointer without its revision is incomplete legacy input, not an empty history'); + assert.throws(() => migrateLegacyStateToV2({ rootDir }), + /Autopilot|revision|orphan|incomplete|missing/i); + assert.deepEqual(fs.readFileSync(pointer), original, 'the original pointer witness must remain intact'); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false, + 'an orphaned legacy pointer must not publish a canonical migration-completed event'); +}); + +test('T04-H73 orphaned orchestration revisions without immutable manifest cannot be certified', (t) => { + const rootDir = tempProject(t, 'dk-v13-h73-orphan-run-'); + const legacy = writeLegacyRun(rootDir); + const revision = path.join(legacy.runDir, 'state-revisions', '00000003.json'); + const before = fs.readFileSync(revision); + fs.unlinkSync(path.join(legacy.runDir, 'manifest.json')); + + assert.throws(() => inspectLegacyState(rootDir), + /orchestration|manifest|orphan|incomplete|missing/i, + 'a surviving revision/pointer without the immutable manifest is an incomplete run'); + assert.throws(() => migrateLegacyStateToV2({ rootDir }), + /orchestration|manifest|orphan|incomplete|missing/i); + assert.deepEqual(fs.readFileSync(revision), before, 'surviving original revisions must remain intact'); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false, + 'orphaned orchestration history must not be recorded as migrated'); +}); From 791c70b1dc6291f0a11cd4a4071b8b9287f216be Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 03:05:02 +0200 Subject: [PATCH 164/177] fix(t04): port validated H74 orphaned orchestration pointer fail-closed guard --- runtime/orchestration/state-engine-migration.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index 7f46e190a..f7c72a1ef 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -383,6 +383,14 @@ function orchestrationHistories(rootDir) { } if (states.length === 0) { + // A surviving legacy pointer is an acknowledged revision witness. + // Never downgrade it to manifest-only revision 1 when its immutable + // revision files are missing, even if final-state.json is absent. + if (fs.existsSync(path.join(runDir, 'current-state.json'))) { + throw new StateMigrationError( + 'Legacy orchestration current pointer exists without revision history', + ); + } // Fresh v0.13 runs retain immutable manifests but persist revisions in // the canonical V2 ledger. A manifest without legacy revision files // is not a legacy revision-1 history when that run already exists in V2. From 33941032894538b3f47a113b5b0655a2872a4939 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 03:05:08 +0200 Subject: [PATCH 165/177] test(t04): port cross-platform RED/GREEN H74 orphaned pointer regression --- scripts/state-engine-v2-migration.test.mjs | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index 7cf5b15d9..a2add156a 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -1540,3 +1540,31 @@ test('T04-H73 orphaned orchestration revisions without immutable manifest cannot assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false, 'orphaned orchestration history must not be recorded as migrated'); }); + + +test('T04-H74 orphaned orchestration current pointer with no revision files cannot certify manifest-only migration', (t) => { + const rootDir = tempProject(t, 'dk-v13-h74-orphan-run-pointer-'); + const legacy = writeLegacyRun(rootDir); + const pointer = path.join(legacy.runDir, 'current-state.json'); + const pointerBytes = fs.readFileSync(pointer); + const manifest = path.join(legacy.runDir, 'manifest.json'); + const manifestBytes = fs.readFileSync(manifest); + const revisionDir = path.join(legacy.runDir, 'state-revisions'); + for (const name of fs.readdirSync(revisionDir)) fs.unlinkSync(path.join(revisionDir, name)); + // Remove the final-state cross-check so the only surviving revision witness + // is the acknowledged current-state pointer. A manifest alone is revision 1, + // not proof that the later current revision can be silently discarded. + fs.unlinkSync(path.join(legacy.runDir, 'final-state.json')); + + assert.throws(() => inspectLegacyState(rootDir), + /orchestration|pointer|revision|orphan|incomplete|missing/i, + 'surviving pointer to absent revisions must not be certified as manifest-only history'); + assert.throws(() => migrateLegacyStateToV2({ rootDir }), + /orchestration|pointer|revision|orphan|incomplete|missing/i); + assert.deepEqual(fs.readFileSync(pointer), pointerBytes, + 'rejected migration must preserve original current pointer'); + assert.deepEqual(fs.readFileSync(manifest), manifestBytes, + 'rejected migration must preserve immutable manifest'); + assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false, + 'rejected migration must not publish canonical layout or completion event'); +}); From e4f254cf991a65224cf65e7516d3b446a0e9b01a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 03:05:17 +0200 Subject: [PATCH 166/177] docs(t04): record H74 test-first cross-platform evidence and outstanding gates --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index a3982a3fa..f90f59601 100644 --- a/memory.md +++ b/memory.md @@ -303,3 +303,5 @@ Integrated through PR #54; Issue #51 is completed. - **T04-H72 legacy symlink/backup completeness (1 November 2026; NOT ACCEPTED):** Independent exact-source PR #92 review at `4f50559ce8cf8c125212c7cea3999bae02859352` found that `collectLegacyBackupFiles()` silently omitted symlinked Autopilot revision/pointer inputs while `autopilotHistory()` followed and imported them, allowing false semantic equivalence, incomplete backup and incomplete source fingerprint. Test-only diagnostic draft PR #162 commit `52b9b3b6f8fb38cbfa70d67a34e2e58c3836fd9d` reproduced both defects on **Ubuntu and Windows** in RED CI `41247622308` (both full validation jobs failed on the expected H72 negative controls). Narrow fail-closed fix rejects symlinks and junctions in scanned legacy source trees and their ancestor paths before backup/fingerprint/cutover. Three H72 regressions (symlinked revision, symlinked pointer and junctioned state root) all passed without skips on both platforms in full GREEN CI `41247716349` on diagnostic source `64ae5a15f82ed58805da79ae0887ceff9e93bfcc`; earlier GREEN `41247698757` also passed both full platforms for the first two tests. Ported only validated runtime blob `d6c5d93f117146db5b24c5372562c0c04c767329` and test blob `174ea49179691686a1d72be9cae463ccbcf728a1` byte-identically into authoritative v0.13 draft PR #92 (runtime commit `2f08f1f7cd685c9043f9110f72a3e5cfbdea431a`, tests commit `08c2be5a3cd56f2cdbdb1ef54e863b67118389be`). This prevents false backup certification for symlinked source inputs; it does not prove hostile same-UID TOCTOU safety, NTFS sudden physical power-loss durability, three independent exact-final-source role approvals or persisted deterministic risk-3 ACCEPTED. Fresh final integration-head dual Ubuntu/Windows validation is required; no merge/release authority. - **T04-H73 orphaned legacy witness fail-closed correction (1 November 2026; NOT ACCEPTED):** Exact v0.13 PR #92 source `e9ba6e3bfcf8d859401f5501e3a1d77178dbcd34` silently treated a surviving Autopilot `current.json` without revision files and orchestration `state-revisions`/pointer without immutable `manifest.json` as empty histories. This could certify an incomplete migration and publish `MIGRATION_COMPLETED` while legacy evidence remained. Diagnostic draft PR #164 committed two adversarial tests first at `5e6f57d1b3b34cb32182987a694eeff9caf1c2a1`; both reproduced as expected failures on Ubuntu and Windows in RED CI `41251264165`. Narrow source fix rejects both incomplete legacy witness classes before canonical layout/backup/cutover; the original legacy files remain intact. Complete Ubuntu and Windows validation passed in GREEN CI `41251304713` on diagnostic commit `6d3228d7c25753b1747cebe5d6ad3220c6b22731`. Runtime source blob `7f46e190a1dcb4dfab8c8c6d81c663edca33bb69` and test blob `7cf5b15d9b6259096992c422bd0f2740c4442c2d` are the exact validated artifacts. Three independent authenticated exact-final-source role approvals, two complete final-head platform runs, physical NTFS sudden-power-loss assurance under Issue #73 and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge or release authority. + +- **T04-H74 orphaned orchestration pointer fail-closed correction (1 November 2026; NOT ACCEPTED):** Actual v0.13 PR #92 source at `bd7bebad9368b81674789cb707dae72bdc388863` accepted a legacy orchestration `current-state.json` pointer to absent revision files when `manifest.json` survived and `final-state.json` was absent. `inspectLegacyState()` silently treated this as a manifest-only revision-1 history, allowing incomplete migration certification. Diagnostic draft PR #166 test-only commit `7dbf790d951d08dcbe7037a1ee9fc8b9afaa6ba4` reproduced the missing fail-closed exception on **both Ubuntu and Windows** in RED CI [#41258831305](https://github.com/eybersjp/development-kit/actions/runs/41258831305). Narrow correction `da1ccaf51b9c47da6ce8624b893ba67fe5b679c7` rejects a surviving pointer without its immutable revision history before canonical layout/backup/cutover; it preserves the original pointer and manifest. Complete Ubuntu and Windows validation passed in GREEN CI [#41258894217](https://github.com/eybersjp/development-kit/actions/runs/41258894217), including H74 and v0.13 compatibility. Ported exact validated source blob `f7c72a1efc867a9af25bfbf3bde70399ee2bb5b4` and regression test blob `a2add156ace088ed9f939ff404b9d3f83d95e817` byte-identically to authoritative PR #92 via commits `791c70b1dc6291f0a11cd4a4071b8b9287f216be` and `33941032894538b3f47a113b5b0655a2872a4939`. Final integration-head two independent full Ubuntu/Windows runs, three distinct authenticated exact-source code/architecture/security approvals, controlled NTFS physical sudden-power-loss evidence (Issue #73) and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge, release or waiver authorized. From c3fb186b8f117d76e259b102b503d46b11605055 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 04:04:08 +0200 Subject: [PATCH 167/177] fix(t04-h75): preserve V2 pending journal on read-only scale-context --- runtime/api/runtime-api-service.mjs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/runtime/api/runtime-api-service.mjs b/runtime/api/runtime-api-service.mjs index c9db273e7..c3824931c 100644 --- a/runtime/api/runtime-api-service.mjs +++ b/runtime/api/runtime-api-service.mjs @@ -22,7 +22,7 @@ import { resolveMemoryIdentity } from '../intelligence/memory-identity.mjs'; import { resolveEffectiveSettings } from '../intelligence/settings.mjs'; import { LocalMemoryProvider } from '../intelligence/local-memory-provider.mjs'; import { getCurrentState } from '../autopilot/state-store.mjs'; -import { isStateEngineV2Active, hasCompletedStateMigration, loadLatestEntityState } from '../orchestration/state-engine-v2.mjs'; +import { isStateEngineV2Active, loadStateSnapshot } from '../orchestration/state-engine-v2.mjs'; import { validateWorkflowState } from '../autopilot/validators.mjs'; import { loadWorkspaceTargetRegistry } from '../orchestration/workspace-targets.mjs'; import { loadCostRecords } from '../orchestration/cost-observatory.mjs'; @@ -293,24 +293,33 @@ export class RuntimeApiService { // Selecting legacy authority must use the non-mutating pointer projection // below: getCurrentState() can repair current.json as a side effect. let v2WorkflowAuthority = v2Active; + let v2Snapshot = null; if (v2Active) { try { + // An observing GET must not replay a pending commit, rebuild derived + // state, or retire crash evidence while selecting V2 authority. + v2Snapshot = loadStateSnapshot(this.rootDir, { rebuildIfNeeded: false }); + if (!v2Snapshot) throw new Error('V2 snapshot is unavailable for read-only projection'); const hasLegacyRevisions = fs.existsSync(directory) && fs.readdirSync(directory).some((name) => /^revision-\d{6}\.json$/.test(name)); if (hasLegacyRevisions) { - v2WorkflowAuthority = hasCompletedStateMigration(this.rootDir) && - loadLatestEntityState('autopilot-workflow', this.rootDir) !== null; + const migrated = v2Snapshot.migrations.some((item) => item.semanticEquivalence === true); + const workflowRecords = v2Snapshot.entities['autopilot-workflow'] ?? {}; + v2WorkflowAuthority = migrated && Object.keys(workflowRecords).length > 0; } } catch { - // An unreadable V2 migration witness cannot authorize fallback. - v2Active = false; + // An interrupted or unreadable V2 authority cannot authorize legacy + // fallback, and read-only GET must leave all evidence untouched. v2Invalid = true; v2WorkflowAuthority = false; } } if (v2WorkflowAuthority) { try { - const revision = getCurrentState(this.rootDir); + const workflowRecords = v2Snapshot.entities['autopilot-workflow'] ?? {}; + const revision = Object.entries(workflowRecords) + .sort((a, b) => b[1].lastEventSequence - a[1].lastEventSequence || + a[0].localeCompare(b[0]))[0]?.[1]?.state ?? null; if (revision) { validateWorkflowState(revision); stateHealth = 'unverified'; From 5bf9ecedd8686096bd7ad6dce9eeffb2c53bffdd Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 04:04:14 +0200 Subject: [PATCH 168/177] test(t04-h75): retain pending V2 crash evidence across scale-context GET --- scripts/state-engine-v013.test.mjs | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/scripts/state-engine-v013.test.mjs b/scripts/state-engine-v013.test.mjs index 21eef07a5..f9a864241 100644 --- a/scripts/state-engine-v013.test.mjs +++ b/scripts/state-engine-v013.test.mjs @@ -990,3 +990,52 @@ test('W73-8DOT3-02 signed long/short destination aliases cannot overwrite staged name.startsWith('.dk-legacy-restore-stage-')), [], 'Rejected prepublication stage must be removed without touching any original root'); }); + + +test('V13-API-PROJECTION-04 interrupted V2 commit must not be recovered by read-only scale-context', async (t) => { + const rootDir = tempDir(t, 'dk-v013-api-pending-readonly-'); + const state = createInitialState({ autonomy: 'guided-autopilot' }, rootDir); + saveStateRevision(state, rootDir); + const paths = getStateEnginePaths(rootDir); + const baseline = captureMaterialized(paths); + + const nextState = { ...state, stateRevision: state.stateRevision + 1, + updatedAt: new Date(Date.parse(state.updatedAt) + 1000).toISOString() }; + saveStateRevision(nextState, rootDir); + const fullEvents = fs.readFileSync(paths.events, 'utf8'); + const suffix = fullEvents.slice(baseline.events.length); + assert.ok(suffix.endsWith('\n') && suffix.length > 0); + + restoreMaterialized(paths, baseline); + const pending = JSON.stringify({ + schemaVersion: '1.0.0', + previousByteLength: Buffer.byteLength(baseline.events), + previousHash: 'sha256:' + crypto.createHash('sha256').update(baseline.events).digest('hex'), + suffix, + }) + '\n'; + fs.writeFileSync(paths.pending, pending, 'utf8'); + const before = { + events: fs.readFileSync(paths.events), + snapshot: fs.readFileSync(paths.snapshot), + index: fs.readFileSync(paths.index), + pending: fs.readFileSync(paths.pending), + }; + + const service = new RuntimeApiService({ rootDir }); + const info = await service.start(); + try { + const response = await fetch(info.url + '/v1/scale-context'); + assert.equal(response.status, 200); + const projection = await response.json(); + assert.ok(['recovery-required', 'degraded'].includes(projection.stateHealth), + 'Read-only GET must expose recovery-required/degraded, not silently commit pending state'); + assert.equal(projection.state.active, false, + 'Unrecovered canonical state must not be projected as a verified workflow'); + for (const [name, bytes] of Object.entries(before)) { + assert.deepEqual(fs.readFileSync(paths[name]), bytes, + 'Read-only GET must preserve exact ' + name + ' evidence'); + } + } finally { + await service.stop(); + } +}); From b77ad0a471eec0c2f7f06a2f254c7357fcb64769 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 04:04:25 +0200 Subject: [PATCH 169/177] docs(t04-h75): record source-bound RED/GREEN recovery-preservation evidence --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index f90f59601..ff66268f8 100644 --- a/memory.md +++ b/memory.md @@ -305,3 +305,5 @@ Integrated through PR #54; Issue #51 is completed. - **T04-H73 orphaned legacy witness fail-closed correction (1 November 2026; NOT ACCEPTED):** Exact v0.13 PR #92 source `e9ba6e3bfcf8d859401f5501e3a1d77178dbcd34` silently treated a surviving Autopilot `current.json` without revision files and orchestration `state-revisions`/pointer without immutable `manifest.json` as empty histories. This could certify an incomplete migration and publish `MIGRATION_COMPLETED` while legacy evidence remained. Diagnostic draft PR #164 committed two adversarial tests first at `5e6f57d1b3b34cb32182987a694eeff9caf1c2a1`; both reproduced as expected failures on Ubuntu and Windows in RED CI `41251264165`. Narrow source fix rejects both incomplete legacy witness classes before canonical layout/backup/cutover; the original legacy files remain intact. Complete Ubuntu and Windows validation passed in GREEN CI `41251304713` on diagnostic commit `6d3228d7c25753b1747cebe5d6ad3220c6b22731`. Runtime source blob `7f46e190a1dcb4dfab8c8c6d81c663edca33bb69` and test blob `7cf5b15d9b6259096992c422bd0f2740c4442c2d` are the exact validated artifacts. Three independent authenticated exact-final-source role approvals, two complete final-head platform runs, physical NTFS sudden-power-loss assurance under Issue #73 and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge or release authority. - **T04-H74 orphaned orchestration pointer fail-closed correction (1 November 2026; NOT ACCEPTED):** Actual v0.13 PR #92 source at `bd7bebad9368b81674789cb707dae72bdc388863` accepted a legacy orchestration `current-state.json` pointer to absent revision files when `manifest.json` survived and `final-state.json` was absent. `inspectLegacyState()` silently treated this as a manifest-only revision-1 history, allowing incomplete migration certification. Diagnostic draft PR #166 test-only commit `7dbf790d951d08dcbe7037a1ee9fc8b9afaa6ba4` reproduced the missing fail-closed exception on **both Ubuntu and Windows** in RED CI [#41258831305](https://github.com/eybersjp/development-kit/actions/runs/41258831305). Narrow correction `da1ccaf51b9c47da6ce8624b893ba67fe5b679c7` rejects a surviving pointer without its immutable revision history before canonical layout/backup/cutover; it preserves the original pointer and manifest. Complete Ubuntu and Windows validation passed in GREEN CI [#41258894217](https://github.com/eybersjp/development-kit/actions/runs/41258894217), including H74 and v0.13 compatibility. Ported exact validated source blob `f7c72a1efc867a9af25bfbf3bde70399ee2bb5b4` and regression test blob `a2add156ace088ed9f939ff404b9d3f83d95e817` byte-identically to authoritative PR #92 via commits `791c70b1dc6291f0a11cd4a4071b8b9287f216be` and `33941032894538b3f47a113b5b0655a2872a4939`. Final integration-head two independent full Ubuntu/Windows runs, three distinct authenticated exact-source code/architecture/security approvals, controlled NTFS physical sudden-power-loss evidence (Issue #73) and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge, release or waiver authorized. + +- **T04-H75 read-only V2 Control Center projection (1 November 2026; NOT ACCEPTED):** Exact authoritative v0.13 PR #92 source `e4f254cf991a65224cf65e7516d3b446a0e9b01a` could replay a valid pending V2 commit and retire `pending-commit.json` during the ostensibly read-only `GET /v1/scale-context`. A new adversarial V13-API-PROJECTION-04 fixture creates a valid pending revision-2 suffix over acknowledged revision-1 events/snapshot/index and asserts that the GET reports degraded/recovery-required without changing any of the four byte-level witnesses. Test-first diagnostic PR #168 RED commit `6f69e3c76c17256eb6ff5019a5208746e8857fc0` failed on **Ubuntu and Windows** in full CI [#41263121147](https://github.com/eybersjp/development-kit/actions/runs/41263121147) on the exact expected negative control; both platform logs identified V13-API-PROJECTION-04 as the sole failing test. Narrow diagnostic PR #169 correction commit `3ff81aa3cb1ef5b09d59ed8242e86c859a5f5f3e` uses `loadStateSnapshot({ rebuildIfNeeded: false })` for both V2 authority selection and workflow projection; it neither replays pending canonical events nor rewrites snapshot/index/journal, and retains the unmigrated v0.12.1 legacy pointer projection. Full Ubuntu and Windows validation passed in GREEN CI [#41263092726](https://github.com/eybersjp/development-kit/actions/runs/41263092726). Ported exact validated API source blob `c3824931c8d8e1ccdb3727b2eb044ab6d47cf8dd` and test blob `f9a86424137ec237eb97e3b75d5e59721638f424` into authoritative PR #92 via commits `c3fb186b8f117d76e259b102b503d46b11605055` and `5bf9ecedd8686096bd7ad6dce9eeffb2c53bffdd`. H75 is a corrected diagnostic finding, not an independent role approval. Two full Ubuntu/Windows validations on the final memory-updated PR #92 SHA, genuine three-role independent source-bound approvals, Issue #73 controlled physical NTFS sudden-power-loss evidence and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge or release authorization. From 80152156ccda8617ef102ded0170c385b50d8c9a Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 04:26:44 +0200 Subject: [PATCH 170/177] fix(t04-h76-h77): port test-first verified scripts/state-engine-v013.test.mjs from PR #173 --- scripts/state-engine-v013.test.mjs | 71 ++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/scripts/state-engine-v013.test.mjs b/scripts/state-engine-v013.test.mjs index f9a864241..e8644e899 100644 --- a/scripts/state-engine-v013.test.mjs +++ b/scripts/state-engine-v013.test.mjs @@ -1039,3 +1039,74 @@ test('V13-API-PROJECTION-04 interrupted V2 commit must not be recovered by read- await service.stop(); } }); + + +test('V13-API-PROJECTION-05 read-only preflight preserves terminated bootstrap staging hard link', async (t) => { + const rootDir = tempDir(t, 'dk-v013-api-bootstrap-readonly-'); + const state = createInitialState({ autonomy: 'guided-autopilot' }, rootDir); + saveStateRevision(state, rootDir); + const paths = getStateEnginePaths(rootDir); + const before = captureMaterialized(paths); + const schemaBytes = fs.readFileSync(paths.schema); + const service = new RuntimeApiService({ rootDir }); + const info = await service.start(); + const deadPid = 2000000000; + const stage = paths.schema + '.tmp-' + deadPid + '-' + crypto.randomUUID(); + try { + fs.linkSync(paths.schema, stage); + assert.equal(fs.statSync(paths.schema).nlink, 2, 'Fixture must reproduce the interrupted bootstrap hard link'); + const response = await fetch(info.url + '/v1/scale-context'); + assert.equal(response.status, 200); + const projection = await response.json(); + assert.ok(['recovery-required', 'degraded'].includes(projection.stateHealth), + 'A read-only projection must not complete bootstrap recovery'); + assert.equal(projection.state.active, false); + assert.equal(fs.existsSync(stage), true, 'GET must retain the exact staging-link crash witness'); + assert.equal(fs.statSync(paths.schema).nlink, 2, 'GET must not unlink a bootstrap staging hard link'); + assert.deepEqual(fs.readFileSync(paths.schema), schemaBytes); + for (const [field, bytes] of Object.entries(before)) { + assert.equal(fs.readFileSync(paths[field], 'utf8'), bytes, + 'GET must preserve ' + field + ' bytes'); + } + } finally { + await service.stop(); + } +}); + +test('V13-API-PROJECTION-06 surviving index must block a read-only canonical history regression', async (t) => { + const rootDir = tempDir(t, 'dk-v013-api-index-witness-'); + const state = createInitialState({ autonomy: 'guided-autopilot' }, rootDir); + saveStateRevision(state, rootDir); + const paths = getStateEnginePaths(rootDir); + const revisionOne = captureMaterialized(paths); + const next = { ...state, stateRevision: state.stateRevision + 1, + updatedAt: new Date(Date.parse(state.updatedAt) + 1000).toISOString() }; + saveStateRevision(next, rootDir); + const acknowledgedIndex = fs.readFileSync(paths.index); + const service = new RuntimeApiService({ rootDir }); + const info = await service.start(); + try { + fs.writeFileSync(paths.events, revisionOne.events); + fs.writeFileSync(paths.snapshot, revisionOne.snapshot); + const before = { + events: fs.readFileSync(paths.events), + snapshot: fs.readFileSync(paths.snapshot), + index: fs.readFileSync(paths.index), + }; + assert.deepEqual(before.index, acknowledgedIndex, 'Later index witness must survive'); + const response = await fetch(info.url + '/v1/scale-context'); + assert.equal(response.status, 200); + const projection = await response.json(); + assert.ok(['recovery-required', 'degraded'].includes(projection.stateHealth), + 'A later index witness must prevent projection of rolled-back history'); + assert.equal(projection.state.active, false); + assert.throws(() => loadStateSnapshot(rootDir, { rebuildIfNeeded: false }), + /canonical.*(truncated|witness|regression)|index.*witness/i); + for (const [field, bytes] of Object.entries(before)) { + assert.deepEqual(fs.readFileSync(paths[field]), bytes, + 'Read-only verification must preserve ' + field + ' evidence'); + } + } finally { + await service.stop(); + } +}); From 5966101ac7c43f3d2f5b94f3554d6138ec547a92 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 04:26:46 +0200 Subject: [PATCH 171/177] fix(t04-h76-h77): port test-first verified runtime/orchestration/state-engine-v2.mjs from PR #173 --- runtime/orchestration/state-engine-v2.mjs | 32 ++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/runtime/orchestration/state-engine-v2.mjs b/runtime/orchestration/state-engine-v2.mjs index 65c2681ea..348c59318 100644 --- a/runtime/orchestration/state-engine-v2.mjs +++ b/runtime/orchestration/state-engine-v2.mjs @@ -104,7 +104,7 @@ function stateRevision(value, { allowNull = false } = {}) { return value; } -function statePaths(rootDir = process.cwd()) { +function statePaths(rootDir = process.cwd(), { recoverBootstrapIfNeeded = true } = {}) { const root = path.resolve(rootDir); const stateRoot = path.resolve(root, STATE_ENGINE_ROOT); const relative = path.relative(root, stateRoot); @@ -123,7 +123,7 @@ function statePaths(rootDir = process.cwd()) { }); if (fs.existsSync(stateRoot)) { assertStateRootRealpathSafe(paths); - assertStateFilesNotLinks(paths); + assertStateFilesNotLinks(paths, { recoverBootstrapIfNeeded }); } return paths; } @@ -138,7 +138,7 @@ function assertStateRootRealpathSafe(paths) { return stateReal; } -function assertStateFilesNotLinks(paths) { +function assertStateFilesNotLinks(paths, { recoverBootstrapIfNeeded = true } = {}) { for (const field of ['events', 'snapshot', 'index', 'schema', 'pending', 'lock']) { try { const stat = fs.lstatSync(paths[field]); @@ -149,7 +149,7 @@ function assertStateFilesNotLinks(paths) { // The common path must not pay for extra filesystem probes on every // state read/write. Reconcile only an actual multiply-linked bootstrap // schema/snapshot/index; all other hard links remain forbidden. - if (['schema', 'snapshot', 'index'].includes(field)) { + if (recoverBootstrapIfNeeded && ['schema', 'snapshot', 'index'].includes(field)) { recoverInterruptedBootstrapPublication(paths); } if (fs.lstatSync(paths[field]).nlink > 1) { @@ -761,8 +761,8 @@ export function ensureStateEngineLayout(rootDir = process.cwd()) { return paths; } -export function isStateEngineV2Active(rootDir = process.cwd()) { - const paths = statePaths(rootDir); +export function isStateEngineV2Active(rootDir = process.cwd(), { recoverIfNeeded = true } = {}) { + const paths = statePaths(rootDir, { recoverBootstrapIfNeeded: recoverIfNeeded }); if (!fs.existsSync(paths.stateRoot)) return false; const schemaExists = fs.existsSync(paths.schema); @@ -1090,7 +1090,7 @@ function repairUnwitnessedTrailingLedgerSuffixLocked(rootDir) { } export function loadCanonicalEvents(rootDir = process.cwd(), { recoverIfNeeded = true } = {}) { - const paths = statePaths(rootDir); + const paths = statePaths(rootDir, { recoverBootstrapIfNeeded: recoverIfNeeded }); if (!fs.existsSync(paths.events)) { assertMissingCanonicalLedgerHasNoStateWitness(paths); return []; @@ -1414,12 +1414,26 @@ export function rebuildStateSnapshot(rootDir = process.cwd()) { } export function loadStateSnapshot(rootDir = process.cwd(), { rebuildIfNeeded = true } = {}) { - const paths = statePaths(rootDir); + const paths = statePaths(rootDir, { recoverBootstrapIfNeeded: rebuildIfNeeded }); if (!fs.existsSync(paths.events)) { assertMissingCanonicalLedgerHasNoStateWitness(paths); return null; } - const canonical = rebuildSnapshotFromEvents(loadCanonicalEvents(rootDir, { recoverIfNeeded: rebuildIfNeeded })); + // An index can independently witness a later acknowledged event even when + // both events.jsonl and snapshot.json have regressed to an older prefix. + // Check it before returning any snapshot, including read-only projections. + // On a concurrent writer's publication, retry only the canonical read; + // a persistent mismatch remains a hard integrity failure, never a rebuild. + let canonicalEvents = loadCanonicalEvents(rootDir, { recoverIfNeeded: rebuildIfNeeded }); + try { + assertDerivedHistoryWitnesses(paths, canonicalEvents, ['index']); + } catch (error) { + if (!['STATE_CANONICAL_HISTORY_REGRESSION', 'STATE_CANONICAL_HISTORY_HASH_CONFLICT'] + .includes(error.details?.code)) throw error; + canonicalEvents = loadCanonicalEvents(rootDir, { recoverIfNeeded: rebuildIfNeeded }); + assertDerivedHistoryWitnesses(paths, canonicalEvents, ['index']); + } + const canonical = rebuildSnapshotFromEvents(canonicalEvents); if (!fs.existsSync(paths.snapshot)) { if (!rebuildIfNeeded) return null; From 51388a47b08f446d2a4ba9163d1c5ff04352a674 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 04:26:48 +0200 Subject: [PATCH 172/177] fix(t04-h76-h77): port test-first verified runtime/api/runtime-api-service.mjs from PR #173 --- runtime/api/runtime-api-service.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/api/runtime-api-service.mjs b/runtime/api/runtime-api-service.mjs index c3824931c..b5aaac435 100644 --- a/runtime/api/runtime-api-service.mjs +++ b/runtime/api/runtime-api-service.mjs @@ -285,7 +285,9 @@ export class RuntimeApiService { let v2Active = false; let v2Invalid = false; try { - v2Active = isStateEngineV2Active(this.rootDir); + // Read-only Control Center preflight must never clean up a terminated + // bootstrap staging hard link or consume its crash-recovery evidence. + v2Active = isStateEngineV2Active(this.rootDir, { recoverIfNeeded: false }); } catch { v2Invalid = true; } From 6b5d31d47db0a07f3eb565dbb93bd5cf97ffe051 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 04:26:56 +0200 Subject: [PATCH 173/177] docs(t04-h76-h77): preserve source-bound RED/GREEN evidence and acceptance boundaries --- memory.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/memory.md b/memory.md index ff66268f8..d5d9c5340 100644 --- a/memory.md +++ b/memory.md @@ -307,3 +307,5 @@ Integrated through PR #54; Issue #51 is completed. - **T04-H74 orphaned orchestration pointer fail-closed correction (1 November 2026; NOT ACCEPTED):** Actual v0.13 PR #92 source at `bd7bebad9368b81674789cb707dae72bdc388863` accepted a legacy orchestration `current-state.json` pointer to absent revision files when `manifest.json` survived and `final-state.json` was absent. `inspectLegacyState()` silently treated this as a manifest-only revision-1 history, allowing incomplete migration certification. Diagnostic draft PR #166 test-only commit `7dbf790d951d08dcbe7037a1ee9fc8b9afaa6ba4` reproduced the missing fail-closed exception on **both Ubuntu and Windows** in RED CI [#41258831305](https://github.com/eybersjp/development-kit/actions/runs/41258831305). Narrow correction `da1ccaf51b9c47da6ce8624b893ba67fe5b679c7` rejects a surviving pointer without its immutable revision history before canonical layout/backup/cutover; it preserves the original pointer and manifest. Complete Ubuntu and Windows validation passed in GREEN CI [#41258894217](https://github.com/eybersjp/development-kit/actions/runs/41258894217), including H74 and v0.13 compatibility. Ported exact validated source blob `f7c72a1efc867a9af25bfbf3bde70399ee2bb5b4` and regression test blob `a2add156ace088ed9f939ff404b9d3f83d95e817` byte-identically to authoritative PR #92 via commits `791c70b1dc6291f0a11cd4a4071b8b9287f216be` and `33941032894538b3f47a113b5b0655a2872a4939`. Final integration-head two independent full Ubuntu/Windows runs, three distinct authenticated exact-source code/architecture/security approvals, controlled NTFS physical sudden-power-loss evidence (Issue #73) and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge, release or waiver authorized. - **T04-H75 read-only V2 Control Center projection (1 November 2026; NOT ACCEPTED):** Exact authoritative v0.13 PR #92 source `e4f254cf991a65224cf65e7516d3b446a0e9b01a` could replay a valid pending V2 commit and retire `pending-commit.json` during the ostensibly read-only `GET /v1/scale-context`. A new adversarial V13-API-PROJECTION-04 fixture creates a valid pending revision-2 suffix over acknowledged revision-1 events/snapshot/index and asserts that the GET reports degraded/recovery-required without changing any of the four byte-level witnesses. Test-first diagnostic PR #168 RED commit `6f69e3c76c17256eb6ff5019a5208746e8857fc0` failed on **Ubuntu and Windows** in full CI [#41263121147](https://github.com/eybersjp/development-kit/actions/runs/41263121147) on the exact expected negative control; both platform logs identified V13-API-PROJECTION-04 as the sole failing test. Narrow diagnostic PR #169 correction commit `3ff81aa3cb1ef5b09d59ed8242e86c859a5f5f3e` uses `loadStateSnapshot({ rebuildIfNeeded: false })` for both V2 authority selection and workflow projection; it neither replays pending canonical events nor rewrites snapshot/index/journal, and retains the unmigrated v0.12.1 legacy pointer projection. Full Ubuntu and Windows validation passed in GREEN CI [#41263092726](https://github.com/eybersjp/development-kit/actions/runs/41263092726). Ported exact validated API source blob `c3824931c8d8e1ccdb3727b2eb044ab6d47cf8dd` and test blob `f9a86424137ec237eb97e3b75d5e59721638f424` into authoritative PR #92 via commits `c3fb186b8f117d76e259b102b503d46b11605055` and `5bf9ecedd8686096bd7ad6dce9eeffb2c53bffdd`. H75 is a corrected diagnostic finding, not an independent role approval. Two full Ubuntu/Windows validations on the final memory-updated PR #92 SHA, genuine three-role independent source-bound approvals, Issue #73 controlled physical NTFS sudden-power-loss evidence and persisted deterministic risk-3 ACCEPTED remain mandatory. No merge or release authorization. + +- **T04-H76/H77 read-only canonical witness hardening (1 November 2026; NOT ACCEPTED):** Source-bound independent Codex review of authoritative PR #92 at `b77ad0a471eec0c2f7f06a2f254c7357fcb64769` identified two P2 faults. H76: `GET /v1/scale-context` could mutate interrupted bootstrap schema hard-link evidence during `isStateEngineV2Active` preflight. H77: a later acknowledged `index.db` witness could be ignored when both `events.jsonl` and `snapshot.json` regressed, allowing stale workflow projection. Draft diagnostic [PR #173](https://github.com/eybersjp/development-kit/pull/173) committed V13-API-PROJECTION-05/06 tests first at `322a4c6ae6d8050682b8fdbeeed63efd21dce308`; both Ubuntu and Windows RED CI [#41264569817](https://github.com/eybersjp/development-kit/actions/runs/41264569817) reproduced both expected failures. Narrow correction `3792c91128cc8b34da356980683968e3059b67fd` separates read-only bootstrap preflight from recovery-capable writes, rejects surviving index sequence/hash regression before returning a snapshot, and retries only a non-mutating canonical read to tolerate concurrent publication. Complete Ubuntu and Windows GREEN CI [#41264602042](https://github.com/eybersjp/development-kit/actions/runs/41264602042) passed, including both new adversarial fixtures and full v0.13 compatibility. Exact validated blobs: tests `e8644e8992350ae1abfac44c84432b9311ccc05b`, State Engine `348c593186f3e40d66fddfde49e17bbcd363a8a6`, runtime API `b5aaac4352e36c1c2649d204ab36aaec56f7926f`. All three ported byte-identically to authoritative PR #92 via commits `80152156ccda8617ef102ded0170c385b50d8c9a`, `5966101ac7c43f3d2f5b94f3554d6138ec547a92`, `51388a47b08f446d2a4ba9163d1c5ff04352a674`. Two full Ubuntu/Windows runs on the final integration SHA, independent exact-source code/architecture/security approvals, physical NTFS sudden-power-loss evidence (Issue #73) and persisted risk-3 ACCEPTED remain mandatory; no merge or release authorized. From 8fa5233986a02acb220eccee44112cb2660a2363 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 05:30:38 +0200 Subject: [PATCH 174/177] test(T04-H78): reject NTFS ADS/device-name backup paths before staging (RED) --- scripts/state-engine-v2-migration.test.mjs | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index a2add156a..980c6a310 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -1568,3 +1568,41 @@ test('T04-H74 orphaned orchestration current pointer with no revision files cann assert.equal(fs.existsSync(path.join(rootDir, '.development-kit', 'state')), false, 'rejected migration must not publish canonical layout or completion event'); }); + + +test('T04-H78 signed backup rejects NTFS alternate streams and reserved device names before staging on every host', async (t) => { + const source = tempProject(t, 'dk-v13-h78-unsafe-winpath-'); + writeLegacyAutopilot(source, 2); + const migrated = migrateLegacyStateToV2({ rootDir: source }); + const original = JSON.parse(fs.readFileSync(migrated.backupPath, 'utf8')); + const { createHash } = await import('node:crypto'); + const canonical = (value) => Array.isArray(value) ? value.map(canonical) + : value && typeof value === 'object' + ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])])) + : value; + const paths = [ + '.development-kit/autopilot/state/revision-000001.json:evil', + '.development-kit/autopilot/state/CON', + '.development-kit/autopilot/state/AUX.txt', + '.development-kit/autopilot/state/bad?.json', + ]; + for (const [i, unsafePath] of paths.entries()) { + const bundle = structuredClone(original); + bundle.files[0].path = unsafePath; + const manifest = bundle.files.map(({ path: entryPath, fingerprint, bytes }) => + ({ path: entryPath, fingerprint, bytes })); + bundle.sourceFingerprint = 'sha256:' + + createHash('sha256').update(JSON.stringify(canonical(manifest))).digest('hex'); + const backup = path.join(source, 'h78-unsafe-' + i + '.json'); + const targetRoot = path.join(source, 'h78-target-' + i); + fs.writeFileSync(backup, JSON.stringify(bundle)); + assert.throws( + () => restoreLegacyBackup({ backupPath: backup, targetRoot, confirmOffline: true }), + /unsafe Windows restore path component/i, + 'portable restore must reject ' + unsafePath + ' before stage creation'); + assert.equal(fs.existsSync(targetRoot), false, + 'rejected Windows-unsafe name must not publish a target tree'); + assert.equal(fs.readdirSync(source).some((name) => name.startsWith('.dk-legacy-restore-stage-')), false, + 'rejected Windows-unsafe name must not create a staging tree'); + } +}); From 1e48500f091000353908dc266ff8bb8d9f8d599b Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 05:31:37 +0200 Subject: [PATCH 175/177] test(T04-H78): include documented superscript-digit DOS device alias (RED) --- scripts/state-engine-v2-migration.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/state-engine-v2-migration.test.mjs b/scripts/state-engine-v2-migration.test.mjs index 980c6a310..eb7431c23 100644 --- a/scripts/state-engine-v2-migration.test.mjs +++ b/scripts/state-engine-v2-migration.test.mjs @@ -1584,6 +1584,7 @@ test('T04-H78 signed backup rejects NTFS alternate streams and reserved device n '.development-kit/autopilot/state/revision-000001.json:evil', '.development-kit/autopilot/state/CON', '.development-kit/autopilot/state/AUX.txt', + '.development-kit/autopilot/state/COM¹.txt', '.development-kit/autopilot/state/bad?.json', ]; for (const [i, unsafePath] of paths.entries()) { From e1f889f1db21f275b960298aafb5d40d2de79a27 Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 05:32:20 +0200 Subject: [PATCH 176/177] fix(T04-H78): reject NTFS ADS and reserved DOS names before restore staging (GREEN candidate) --- runtime/orchestration/state-engine-migration.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index f7c72a1ef..fba221dab 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -195,6 +195,17 @@ function validateBackupBundle(bundle) { throw new StateMigrationError('Legacy backup path contains ill-formed Unicode'); } const pathParts = entry.path.split(/[/\\]+/).filter(Boolean); + // Backups must restore safely on the supported Windows NTFS profile, + // even when validated on a POSIX host. Win32 interprets ':' as an + // alternate-data-stream separator and reserves DOS device basenames, + // including the superscript-digit COM/LPT aliases. Reject these before + // creating any stage, journal, or destination directory. + // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file + const reservedDevice = /^(?:CON|PRN|AUX|NUL|COM[1-9¹²³]|LPT[1-9¹²³])(?:\\.|$)/i; + if (pathParts.some((part) => + /[<>:"|?*\\x00-\\x1f]/u.test(part) || reservedDevice.test(part))) { + throw new StateMigrationError('Unsafe Windows restore path component: ' + entry.path); + } if (pathParts.some((part) => part !== '.' && part !== '..' && (part.endsWith('.') || part.endsWith(' ')))) { throw new StateMigrationError('Legacy backup path contains trailing dot or space: ' + entry.path); } From ca5c4bab8fff68d4e11fb5ca6028c5f0f631fe8d Mon Sep 17 00:00:00 2001 From: Juan-Pierre Eybers Date: Sun, 1 Nov 2026 05:32:31 +0200 Subject: [PATCH 177/177] fix(T04-H78): use exact Win32 forbidden-character and device-name regexes --- runtime/orchestration/state-engine-migration.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/orchestration/state-engine-migration.mjs b/runtime/orchestration/state-engine-migration.mjs index fba221dab..a6cbe5c26 100644 --- a/runtime/orchestration/state-engine-migration.mjs +++ b/runtime/orchestration/state-engine-migration.mjs @@ -201,9 +201,9 @@ function validateBackupBundle(bundle) { // including the superscript-digit COM/LPT aliases. Reject these before // creating any stage, journal, or destination directory. // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file - const reservedDevice = /^(?:CON|PRN|AUX|NUL|COM[1-9¹²³]|LPT[1-9¹²³])(?:\\.|$)/i; + const reservedDevice = /^(?:CON|PRN|AUX|NUL|COM[1-9¹²³]|LPT[1-9¹²³])(?:\.|$)/i; if (pathParts.some((part) => - /[<>:"|?*\\x00-\\x1f]/u.test(part) || reservedDevice.test(part))) { + /[<>:"|?*\x00-\x1f]/u.test(part) || reservedDevice.test(part))) { throw new StateMigrationError('Unsafe Windows restore path component: ' + entry.path); } if (pathParts.some((part) => part !== '.' && part !== '..' && (part.endsWith('.') || part.endsWith(' ')))) {