feature-factory 0.8.6 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/WORKFLOW.md CHANGED
@@ -177,7 +177,8 @@ reporting anything:
177
177
  A park that completes only step 1 is an unreported park with no recovery evidence, which is the state
178
178
  this sequence exists to prevent. Verify step 2 the way an outside observer would: qualified status
179
179
  reports `park_snapshot` as the published path, or `null` when no snapshot exists. Status reports the path
180
- only while the snapshot is a complete copy of the live plane by the step 3 inventory and its manifest still
180
+ only while the snapshot is a complete copy of the live plane by the step 3 inventory — `factory.lock`
181
+ excluded, since a heartbeat may land between the copy and the read — and its manifest still
181
182
  matches the live one byte for byte, so `null` also covers an interrupted or altered copy and a snapshot
182
183
  left by an earlier park: neither is evidence for this park. Publishing again is what makes it correspond.
183
184
 
@@ -243,7 +244,12 @@ and "clean up the prior copy" are contradictory instructions once that rename ha
243
244
  outside `P`; do not copy slice worktrees or any other part of `S`.
244
245
  3. **Verify.** Build source and destination inventories exactly as the completed archive does — every
245
246
  entry's relative path, type and mode, a SHA-256 for each regular file, a link target for each symlink,
246
- sorted lexically — and require exact equality. An unverified staging tree is never published.
247
+ sorted lexically — and require exact equality, **excluding the plane-root `factory.lock` only**. That
248
+ one entry is session liveness rather than run state and is the only thing in the plane designed to
249
+ change on a timer, so comparing it fails whenever a heartbeat lands between reading the source and
250
+ reading the copy. The exclusion is that exact path and nothing else: a `factory.lock` anywhere below
251
+ the plane root is run state and must match. Qualified status excludes the same single path for the same
252
+ reason. An unverified staging tree is never published.
247
253
  4. **Commit.** With no snapshot at the canonical path, rename `.staging-$R` onto it; that rename is the
248
254
  commit point. With one present, first rename the canonical snapshot to `.prior-$R`, then rename
249
255
  `.staging-$R` onto the canonical path; that second rename is the commit point. If the first rename
package/bin/factory.js CHANGED
@@ -23,7 +23,7 @@ import { resolveSpawnExecutable } from "../core/executable.js";
23
23
  import { dispatchInitPublication } from "./init-publication.js";
24
24
  import { CONTROL_PLANE, SCHEMA_VERSION, GATE_NAMES, GATE_STATUSES, MODES, SLICE_STATUSES, STEP_STATUSES, TERMINAL_STATUSES, repositoryRelativePath, validateRun } from "../state/schema.js";
25
25
  import {
26
- claimSessionLock, inspectSessionLock, refreshSessionLock, releaseSessionLock, SessionLockHeldError,
26
+ claimSessionLock, inspectSessionLock, refreshSessionLock, releaseSessionLock, SESSION_LOCK_FILE, SessionLockHeldError,
27
27
  } from "../state/session-lock.js";
28
28
 
29
29
  export const COMMANDS = Object.freeze({
@@ -175,6 +175,15 @@ function briefDigestFor(decision, state, runDir) {
175
175
  function planeInventory(root) {
176
176
  const entries = [];
177
177
  const record = (rel, full) => {
178
+ // The session lock is liveness, not run state, and it is the one entry in the plane designed to change
179
+ // on a timer. Comparing it made every snapshot invalid within one heartbeat: a live park published a
180
+ // complete, byte-correct plane and `status` reported `park_snapshot: null` eleven seconds later,
181
+ // because the copy held `heartbeat_at` 23:28:25 and the plane had moved to 23:28:36. The whole point
182
+ // of the field was to answer "did the driver publish it", and it answered "no" for a snapshot that was
183
+ // there -- the same disagree-with-your-own-description defect the 0.8.3 work existed to remove,
184
+ // reintroduced by the check built to remove it. Every existing test publishes and reads back with no
185
+ // heartbeat in between, which is why three review rounds and a real park all missed it.
186
+ if (rel === SESSION_LOCK_FILE) return;
178
187
  const stat = lstatSync(full);
179
188
  const mode = (stat.mode & 0o7777).toString(8);
180
189
  if (stat.isSymbolicLink()) entries.push(`${rel} l ${mode} ${readlinkSync(full)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feature-factory",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "Durable, observed control plane for /feature runs. Host-agnostic: no opencode dependency.",
5
5
  "type": "module",
6
6
  "license": "MIT",