[Spike] S0: durable session/state persistence provider evaluation - #91
[Spike] S0: durable session/state persistence provider evaluation#91KayUnkroth wants to merge 27 commits into
Conversation
…h-kill rigor Harness work across three areas. Eligibility - Add src/eligibility.mjs; the candidate comparison now decides eligibility over the whole scenario catalog. An unexecuted, Blocked, or Incomplete absolute gate counts as missing evidence, not a pass, so a configuration reads Yes only when every absolute gate passed (otherwise No/Incomplete). Previously the comparison judged eligibility over the executed slice only and could report "Yes" while absolute gates were unexecuted. New locally-executable absolute gates (each with a negative-control mutant) - COR-004: a permission-denied or unreadable existing workspace fails closed and is never treated as an absent/new store (injectReadFault). - HYD-003: an indeterminate (planned) journal is surfaced and must be reconciled before any further mutation is accepted (needsReconciliation, assertReconcilable, reconcileJournal). - MIG-001/002/003: v0->v1 migration is transactional/resumable, idempotent, and fails closed on an unsupported source version; the original is preserved (seedLegacy/migrate, createLegacyWorkspaceV0/migrateWorkspaceV0ToV1). - IMP-001/002: a checksummed legacy envelope validates completely before an atomic import; corrupt/incomplete/duplicate input leaves no partial destination (importEnvelope, checksumWorkspace). - REC-005: recovery diagnostics identify the affected workspace without leaking document bodies or credential-shaped values (diagnostics()). - SEC-001/002/004/005/006: preflight coordinate/identity/secret gating and ownership-checked cleanup are wired as scenario results. Crash and scale rigor - CRS-002 (alias/index) and CRS-003 (restore) now use a real fork + SIGKILL for durable adapters, matching CRS-001, via new store-worker add-alias and restore-crash modes; the in-memory adapter keeps the in-process fault. - BCK-001 adds a real kill mid atomic-replace (fs-atomic onBeforeRename hook): the torn temp file is never read back as state and the previous generation survives. - CON-005 exercises bounded contention across independent OS processes. - PER-001/002/003 sweep small/medium/stress; the comparison reports one block per scale. Repetition is reduced at larger scales to stay within budget. Results (Windows/NTFS, local backing path only) - local-cas: 38 absolute gates pass; local-sqlite: 37 pass, 1 unresolved (no external lock file). Both remain Incomplete: the 14 unexecuted absolute gates are the OneDrive/ADO/GitHub and shared-backing scenarios that require a provider sandbox. Suites: harness 12, detection-power 19, durable-detection 5.
One IWorkspaceStore contract now fronts every backing path, plus a real OneDrive/Graph transport verified live. Backing-path facade - BackingPathStore fronts an inner engine and records the backing-path operation it issues (connect, put-workspace with an if-none-match/if-match precondition, get-workspace, ...). Local no longer bypasses the abstraction: the local-cas and local-sqlite engines run behind the same facade as the providers, so every backing path shares one contract. Transparent to the local candidates (comparison unchanged). Provider scaffold (dry-run, fail-closed) - Provider backing paths are preflight-gated: with no live sandbox they make zero network calls, dry-run by recording the exact operation manifest they would issue, and refuse any live call with a typed fail-closed error. - provider-gates.mjs marks the provider/shared-backing gates Blocked with the precise prerequisite each waits on (not a bare "not executed"); the runner reports them Blocked. - provider-preflight-sheet.mjs emits the non-secret approval sheet + dry-run operation manifest for a provider config; CLI --preflight-sheet. - Full-catalog eligibility is unchanged from the prior commit; the comparison and coverage keep disclosing provider gates honestly. OneDrive transport - OneDriveGraphStore implements the contract over Microsoft Graph. Each workspace is one drive item under a per-run subfolder tippani-s0/<runId>; CAS is provider-native (create with conflictBehavior=fail, update with If-Match on the item eTag). A losing concurrent write is re-read and classified as a typed stale-writer conflict when the generation advanced. Two modes on one code path: dry-run records intended Graph requests with zero network calls; live issues them with a runtime-supplied token. Fully host-agnostic - driveId, folder, and token come from the environment, so no coordinate or credential lives in the repo. cleanup() deletes only the run's own subfolder. Tests - onedrive-dryrun.test.mjs: dry-run zero-network + manifest, dry-run CAS, OneDrive-accurate preflight sheet, live-fails-closed-without-token, and an offline fake-Graph proof of the live path (create/read round-trip, If-Match advance, and a genuine competing-write 412 -> typed conflict). - provider-dryrun.test.mjs: fail-closed, dry-run zero-calls, non-secret sheet, and provider gates published as Blocked with reasons. - onedrive-live-smoke.mjs: an env-driven live smoke that proves BCK-002 end to end - create, If-Match CAS advance, a two-client ETag race yielding exactly one winner and one typed stale-writer conflict, and per-run cleanup. Verified green against a real OneDrive/SharePoint drive with a single identity; the two-user collaboration gates remain deferred. Spec - Added a "Provider architecture direction" section: S0 commits to a provider architecture (one contract, every backing path) for flexibility to accommodate future collaboration. Suites: harness 12, detection-power 19, durable-detection 5, provider-dryrun 8, onedrive 7.
Nine provider-backed gates now execute live against a real OneDrive/SharePoint
drive with a single identity, behind the existing IWorkspaceStore contract.
Transport (OneDriveGraphStore)
- Fault injection: injectFault(kind, {skip}) for throttle (429), auth-expiry
(401), outage (network throw), and lost-response (the write lands but the
acknowledgement is lost). `skip` targets a later call in a sequence, e.g. the
PUT after a compare-and-swap's two reads.
- Version-history recovery: readGeneration() walks the drive item's versions to
return an earlier generation.
- Offline cache: goOffline / stageOffline / reconnect. A staged write is pending
until reconnect confirms it via CAS; it never silently overwrites newer
authority.
- deleteWorkspace() and a cleanup() that also removes the shared tippani-s0
namespace folder once its last run subfolder is gone.
Gates (onedrive-gates.mjs)
- S0-BCK-002 ETag CAS stale-writer, S0-BCK-005 no success-shaped state on
provider failure, S0-COL-004 lost-response reconcile without duplicate,
S0-COL-005 offline-pending-until-CAS, S0-REC-003 recovery after outage,
S0-REC-004 offline cache reconciles against newer authority, S0-MIG-004
local-to-OneDrive rehome preserving WorkspaceId, S0-BKP-003 version-history
recover, S0-BKP-004 restore one authoritative head.
- Each gate runs only against a live OneDrive backing path; anywhere else it
reports Blocked with the gate's precise prerequisite, so the same catalog id
stays honest across configurations.
Harness
- The runner reports a scenario's detail.blocked as Blocked, and tears down a
live provider run's per-run namespace after the run.
- CLI --run-id override gives each live run its own subfolder.
- Host-agnostic: driveId, folder, and token all come from the environment, so no
coordinate or credential is in the repo. config/provider-onedrive-live.json
carries only placeholders.
Tests
- onedrive-gates.test.mjs exercises every gate against an in-memory fake of the
Graph drive (with version history, delete, and replace) and asserts each is
Blocked outside a live OneDrive context.
- Suites: harness 12, detection-power 19, durable-detection 5, provider-dryrun 8,
onedrive 7, onedrive-gates 10.
Verified live: all nine gates pass against a real drive with a single identity;
the run's namespace is cleaned up afterward. The two-user collaboration gates
remain deferred.
Adds an Azure DevOps (Git) backing path behind the common IWorkspaceStore contract, with the same nine single-identity provider gates proven live. Transport (AdoGitStore) - Each workspace is a file on a per-run branch tippani-s0/<runId>; the branch tip commit is the CAS token. A push carries oldObjectId = the tip we read, so a non-fast-forward push (the ref moved) surfaces as a typed stale-writer conflict. The default branch is never touched. - Version-history recovery via commit history, plus fault injection, an offline cache, per-workspace delete, and cleanup that deletes the per-run branch ref - mirroring the OneDrive transport. - Host-agnostic: org, project, repo, and token come from the environment. Gates - The live gate guard now accepts any provider backing path (OneDrive/ADO/ GitHub), and the CAS stale-writer gate is registered under both S0-BCK-002 (OneDrive) and S0-BCK-003 (ADO). The eight generic gates (BCK-005, COL-004/ 005, REC-003/004, MIG-004, BKP-003/004) run unchanged against either transport. Harness / config - registry routes the ado adapter to AdoGitStore; the preflight sheet uses it for an ADO-accurate dry-run manifest. config/provider-ado-live.json selects the ADO gates. - The generic dry-run/fail-closed coverage moves to a new GitHub config (provider-github-dryrun.json): GitHub is the only remaining generic-scaffold provider now that OneDrive and ADO have real transports. Tests - ado-gates.test.mjs exercises every gate against an in-memory fake of the ADO Git REST API (refs, items, pushes with oldObjectId CAS, commit history, ref delete) and asserts each is Blocked outside a live provider context. - Suites: harness 12, detection-power 19, durable-detection 5, provider-dryrun 8, onedrive 7, onedrive-gates 11, ado-gates 11. Verified live: all nine ADO gates pass against a real Git repo with a single identity; the per-run branch is deleted afterward and the default branch is untouched.
- GitHubRepoStore: Contents-API blob-sha CAS on a per-run branch; the repository default branch is never touched. Env-driven coordinates (owner/repo/token) so it stays host-agnostic. - Monotonic observed-generation reads tolerate GitHub read-after-write replication lag: a read never observes a generation below one already seen or written, and a 409 blob-sha precondition failure is treated as the authoritative concurrency signal (re-read past the expectation and surface a typed conflict). Lost-response writes record their durability. - Registered the github adapter with live and dry-run configs, plus an offline fake-GitHub gate suite proving the transport without a repo. - Fixed a latent workspaceId slug-truncation collision in the shared provider gate seeds (distinguishing tag now survives the 32-char slug). All nine single-identity provider gates pass live against a real private repository; per-run branch cleanup and an untouched default branch verified.
…the facade - README: five adapters (two local candidates plus three provider transports); add a Provider live results section (nine single-identity gates pass 9/9 on OneDrive, ADO, and GitHub against approved synthetic-only sandboxes); note the gates still Blocked pending a second identity or perf pass (two-user COL-002/003/006, synced-folder BCK-006, provider performance PER-004); correct the test-suite list. - S0 spec: the local backing path now explicitly runs through the same backing-path facade as the providers (not a special case); the provider section reflects the implemented two-mode (dry-run/live) transport and the live single-identity gate passes instead of a dry-run-only scaffold.
# Conflicts: # package.json
Adds outcome, comparison, redacted preflight, and raw result artifacts for every executed configuration: local CAS/SQLite/reference, the ADO dry-run, and the live OneDrive/ADO/GitHub provider runs. All artifacts are synthetic-only with credentials and provider coordinates redacted -- identity appears only as a runtime label and provider namespaces are per-run tippani-s0 ids. Verified clean by a secret/coordinate/GUID/email scan before commit.
Adds a reviewer-approved N/A status distinct from Incomplete: an invariant that does not apply to an adapter contract is recorded as N/A, not missing evidence. REC-002 (external stale-lock reclamation) does not apply to SQLite, which owns locking internally and recovers a killed writer through its own journal on open. Threaded through the runner ({na} result), eligibility (N/A neither passes nor blocks), and the comparison (N/A column + listing). SQLite absolute gates are now 37 passed / 0 unresolved / 1 N/A / 14 not-executed; CAS keeps its external-lock Pass. Regenerated the local comparison and outcomes; new harness regression asserts an N/A gate is not-applicable, not unresolved, and does not block eligibility.
|
The harness evidence is useful, but the comparison is evaluating adapters rather than architecture mappings. That makes local candidates fail provider-only gates and provider transports fail local-only gates, so every row is necessarily incomplete. Please regenerate this as an applicability-aware matrix across the five engine/backing-path configurations, include the live OneDrive/ADO/GitHub results already on the branch, and keep relative metrics explicitly non-decisional until an eligible mapping exists. |
|
Adding the detail behind my summary above: 1. Eligibility is being calculated at the wrong levelThe local CAS and SQLite configurations are marked incomplete because provider-only gates were not executed inside those configurations. The OneDrive, ADO, and GitHub configurations are likewise marked incomplete because local-engine gates were not executed inside them. Under that model, no component can ever become eligible. S0 explicitly permits a hybrid mapping: one local engine plus provider-native CAS transports behind 2. The comparison contradicts evidence already on the branch
Please regenerate the comparison from all five outcome reports and distinguish 3. Relative ranking violates the report’s own ruleThe comparison says relative metrics rank only configurations that pass every applicable absolute gate, but it ranks two configurations labeled Until an eligible mapping exists, keep these measurements clearly labeled as provisional diagnostics rather than a ranking. 4. The performance evidence isn’t decision-grade yetThe README describes the local measurements as single-run and indicative. The scenario definition requires common repetitions, warm-up, timing method, and reported statistics. The provider reports emit no performance measurements, despite operation durations already landing in seconds rather than milliseconds. Before performance influences the ADR, add repeated runs, variability, environment details, provider requests/bytes, throttling behavior, collaborator-discovery latency, and the common complexity/operability rubric. 5. There is no decision-ready handoffS0 exists to select the persistence architecture before R1. The comparison currently has no recommended mapping, conditions, owners, sign-off, or links from summary cells to raw evidence. My requested revision is an applicability-aware matrix across local SQLite, local envelope, OneDrive envelope, ADO envelope, and GitHub envelope, followed by the candidate architecture mapping, exact blocked gates, owners, and evidence required to close each condition. That would make this reviewable as an ADR input rather than only a test-results summary. |
|
Pushed the complete PR-feedback revision at Review feedback addressed
ResultAll five configurations pass every applicable absolute gate. Both mappings are eligible. The accepted ADR selects hybrid local SQLite plus provider-native generation-CAS transports. Validation: |
|
The original PR description is now superseded by the revision above: it still mentions single-identity coverage and the pre-revision file count. The current reviewer entry points are the accepted ADR, architecture handoff, and completed README checklist linked in the previous comment. |
- reject stale, incomplete, and unapproved experiment evidence - enforce durable identity, safety, cleanup, and replay checks - withdraw the unsupported architecture selection pending reruns Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make stale-lock reclamation crash-safe, version and upgrade durable checksums, and stabilize worker initialization barriers with actionable diagnostics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bind rotated credentials to approved identities, meter response bodies with indeterminate-write handling, fail closed on unsupported GitHub cleanup, and add durable queue conflict resolution with real restart evidence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Requirement 1 (retained evidence): S0-BCK-006 synced-folder evidence is no longer spliced into the OneDrive aggregate by a partial identity check that bypasses campaign validation. aggregate-campaigns now resolves the authoritative sync config, validates the sync run's full evidence identity (source, catalog, applicability, and config revisions), and stores a `separateSync` record with the retained raw/report artifact digests. compare resolves that record and independently re-verifies the identity, config binding, and on-disk raw/report digests. Adds negative tests for a changed artifact, a deleted artifact, a mismatched config, and a missing record, plus an aggregate test that stale sync evidence is rejected. Requirement 2 (runtime docs): README now enumerates `S0_ONEDRIVE_SYNC_ROOT` and every variable the separate sync run needs, clarifies that provider identity is credential-derived (including for the sync run), and the obsolete `S0_ONEDRIVE_IDENTITY` instruction is removed from onedrive-live-smoke. Requirement 3 (SQLite S0-CON-003): README and REVISION-PLAN now explicitly mark the `BEGIN IMMEDIATE` global write serialization as a known structural failure that a rerun alone cannot close; closure requires a criterion revision or an independently approved rationale-backed N/A. Requirement 4/5: honest pending-rerun state preserved (no selected mapping, exit condition unmet, stale evidence rejected); comparison artifacts regenerated once and are deterministic except a single generation timestamp. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Verify existing GitHub run branches with immutable ownership markers, keep cleanup under the approved shared budget and deadline, and atomically retain recoverable cleanup manifests with digested evidence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…atform results Integrated-review finding 1 (S0-BCK-006 credibility): the separate synced-folder run can no longer Pass for an arbitrary local directory or a default/unverified sync client. provider-onedrive-live gains a `syncProfile`; preflight resolves and binds the approved sync root, sync-client identity, and required client state into an effective `syncTargetHash` (excluding the observed state so an unverified client cannot match). A new pure `assessSyncedFolderEvidence` gate refuses: - an unbound or mismatched sync root/identity (arbitrary directory) -> Blocked; - a default 'running'/unverified sync-client state -> Blocked; - a same-device handle probe with no observable behavior -> Incomplete, not Pass. Closure requires two independent sync clients or retained conflict/recovery evidence. New test/synced-folder.test.mjs proves an arbitrary directory and a default 'running' state cannot pass and is wired into spike:s0:test. Aggregate and compare validation stay compatible with the honest Incomplete/Blocked result (S0-BCK-006 is a relative criterion and is verified via the separateSync record). Integrated-review finding 2 (cross-platform honesty): the Windows persistence spike no longer claims the local harness was unchanged. Windows/macOS/Linux rows are marked Historical - pending rerun (invalidated) because the shared contract, checksum, locking, migration, result schema, and the synced-folder gate changed. This aligns with the ADR (cross-platform rerun required), README rerun checklist, and the fail-closed comparison. Comparison artifacts regenerated once; deterministic except the generation timestamp. Full spike suite passes; aggregate and compare --use-existing remain expected-failing (exit 1); git diff --check clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Share the run budget with provider child processes, allow GitHub attach after default-branch movement, and persist cleanup state transitions for safe absent-target and post-delete recovery. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…bind aggregates Final-review finding 1 (synced-folder proof): a Pass can no longer come from an environment client count or an arbitrary JSON self-report. S0_SYNC_INDEPENDENT_CLIENTS is removed as an input; the only credible closure is a structured, signed retained cross-client evidence artifact validated by validateCrossClientEvidence: bound to the approved syncTargetHash and config revision, with >=2 distinct immutable client IDs, per-client observed timestamps/operations, a conflict/recovery outcome, and approval metadata whose digest (crossClientEvidenceDigest) matches the artifact body. Any gap yields Incomplete/Blocked, never Pass. Because no credible automatic proof exists in this environment, Pass is unreachable without that out-of-band artifact and the required future probe (two independent sync clients on separate devices producing a signed artifact) is documented in README and the Windows spike. Tests prove a self-reported count and an unbound conflict JSON cannot pass, and exercise digest tamper, stale config revision, duplicate IDs, and missing-outcome rejections. Finding 2 (semantic binding of linked evidence): compare no longer trusts a byte hash alone. verifyLinkedCampaigns parses every linked campaign raw, validates its source/catalog/applicability/config identity, configurationId, runId, and result schema, and recomputes each aggregate scenario status (shared combineStatuses) and per-campaign positions against the linked runs. verifySeparateSync likewise parses the sync raw and binds the aggregate S0-BCK-006 status to it. Negative test: an aggregate Pass linked to a valid-schema but unrelated/stale raw (matching byte digest) fails on both identity and recomputed status. Finding 3 (config revision): decisionConfigRevision now includes a normalized sandbox.syncProfile, so changing requiredClientState or requireIndependentClients invalidates evidence. Regression test added. No-selected-mapping and stale-evidence rejection preserved; comparison regenerated once (deterministic except the generation timestamp). Full spike suite passes; aggregate and compare --use-existing remain expected-failing (exit 1); diff --check clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create nonce-bound OneDrive and ADO ownership markers before workspace mutations, persist their identity in cleanup manifests, and verify them before preparing or executing idempotent cleanup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ate recompute Final trust-boundary fixes. 1) Distinct sync approval, validated before any write. resolveSyncApproval binds a separate synced-folder approval record (S0_SYNC_APPROVER/APPROVED_AT/ APPROVAL_REFERENCE/TARGET_HASH) whose target hash must equal the computed syncTargetHash and must never reuse sandbox.approval.targetHash. The scenario now runs assessSyncPreconditions (binding, sync-client identity/state, distinct non-future approval) BEFORE sameDeviceSyncProbe, so no filesystem write happens until the approval and binding are validated. 2) Detached signature instead of a forgeable digest. New src/sync-evidence.mjs verifies a detached signature with Node stdlib crypto against a trusted public key whose SPKI fingerprint is pinned into syncProfile.trustedSignerFingerprint (and thus the config revision). The unkeyed SHA digest is gone; future observed/approval timestamps are rejected. If no trusted key is configured, Pass is unreachable and the result is Incomplete. Tests use an ephemeral Ed25519 keypair: a valid signature passes; fabricated/re-signed-by-wrong-key, fingerprint-mismatch, tampered body, future timestamps, stale config revision, duplicate IDs, and missing-key all fail. 3) Compare recomputes complete aggregate claims. verifyLinkedCampaigns now parses every linked campaign raw, validates identity/schema, and re-runs combineResults to compare status, measurements/distributions, and per-campaign positions/evidence, plus campaign approvals against each linked preflight. verifySeparateSync retains the full signed proof and re-invokes validateCrossClientEvidence/signature verification during comparison. Negative tests cover a 0ms aggregate against 100ms raws, bogus campaign keys, an unsupported status, an approval mismatch, empty sync evidence claimed Pass, and a stale linked identity. No-selected-mapping and stale-evidence rejection preserved; comparison regenerated once (deterministic except the generation timestamp). Full spike suite passes; aggregate and compare --use-existing remain expected-failing (exit 1); diff --check clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pass the persisted cleanup nonce and immutable marker context through provider worker arguments so enforcePreflight child stores reuse the parent manifest identity and shared IPC budget. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… canonical aggregate compare; require Ed25519 1) Independent retained authorization + retained validation time. The synced-folder run now retains its own syncTargetHash, the full structured syncApproval, config revision, signer fingerprint, signer public key, and the pre-write validation time in a syncAuthorization block on the raw S0-BCK-006 result and on the aggregate separateSync record. New verifyRetainedSyncProof revalidates the signed proof against those independent retained values — never proof.syncTargetHash — and reuses the retained validation time as "now", so waiting cannot make future-dated evidence valid. It re-checks the approval target/approver/date/reference, signer fingerprint, and config revision. 2) Canonical full aggregate comparison. verifyLinkedCampaigns now compares the COMPLETE recomputed campaign result canonically (status, measurements/distributions, raw samples, request/retry/throttle totals, complexity, N/A approvals, positions, and all evidence), excluding only durationMs, and recomputes campaignVariability. verifySeparateSync compares the complete linked S0-BCK-006 result (excluding aggregate-only annotations), not status only. Negative tests mutate evidence.total, raw samples, campaignVariability, and the retained sync approval and all fail. 3) Ed25519-only signatures. sync-evidence.mjs requires publicKey.asymmetricKeyType === 'ed25519'; RSA/EC keys fail before signature verification. RSA and EC regression tests added. Pending mapping preserved; comparison regenerated once (deterministic except the generation timestamp). Full spike suite passes; aggregate and compare --use-existing remain expected-failing (exit 1); diff --check clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…; always require campaignVariability Finding 1 — bind the authorization context into the signature. The signed Ed25519 payload now covers syncTargetHash, configRevision, signer fingerprint, the sync approval (targetHash/approver/date/reference), validatedAt, and the clients/outcomes; validateCrossClientEvidence requires approval.targetHash to bind the sync target and bounds every observed/approval timestamp by validatedAt (and the caller clock). validatedAt is sourced from the signed artifact, not generated. The retained authorization is derived from the signed proof and stored identically on the raw run and the aggregate separateSync record. verifyRetainedSyncProof now requires the linked and separate copies to be canonically identical, requires the authorization to be bound to (derived from) the signed proof, rejects validatedAt after the linked run completedAt or the current time, and rejects future observed/approval timestamps using the signed validatedAt capped by the current clock — so a 2099 proof with a fabricated 2100 receipt fails today. Tests cover the valid current receipt, the 2099+2100 case, altered retained approval/time, linked/separate mismatch, and empty proof. Finding 2 — verifyLinkedCampaigns always requires and canonically compares campaignVariability; a missing field is an error, and the recomputed value is an explicit empty object when appropriate. Negative test deletes the field. Pending mapping preserved; comparison regenerated once (deterministic except the generation timestamp). Full spike suite passes; aggregate and compare --use-existing remain expected-failing (exit 1); diff --check clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dAt, and type-check campaignVariability Finding 1 — the signed proof approval must canonically equal the runtime sync approval. validateCrossClientEvidence takes an expectedApproval and rejects any approver/date/reference/targetHash mismatch; assessSyncedFolderEvidence passes the runtime syncApproval. The scenario now fully validates the signed proof (including this match) BEFORE the sameDeviceSyncProbe/write, so a proof approved for a different approval is rejected before any mutation and before the retained authorization is derived. Tests: signed approval A + runtime approval B fails; exact match passes. Finding 2 — verifyRetainedSyncProof requires a valid finite linkedCompletedAt for every retained Pass; a null or unparseable value is an error and the validatedAt upper bound is never skipped. Tests cover null and an invalid string. Finding 3 — verifyLinkedCampaigns requires campaignVariability to be a non-null plain object (not an array) and compares it directly with the recomputed object; null/false/0/string/array all fail even when the recomputed variability is empty. New test iterates those values against an empty recomputed variability. Comparison regenerated once (deterministic except the generation timestamp). Full spike suite passes; direct `node src/compare.mjs --use-existing` and direct `node src/aggregate-campaigns.mjs` both exit 1 with decision Incomplete and mapping null; diff --check clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Record the final repaired source identity while keeping the architecture decision incomplete and unselected pending genuine campaign reruns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Draft architecture spike — no production runtime integration.
Outcome
S0 now provides an applicability-aware, decision-ready evaluation of five engine/backing-path configurations:
Every configuration passes all applicable absolute gates. Both candidate architecture mappings are eligible. The accepted decision is hybrid local SQLite plus provider-native generation-CAS transports behind
IWorkspaceStore.Review revisions
Pass,Fail,Blocked,Incomplete,N/A,Not applicable, andNot executedremain distinct.COL-002,COL-003, andCOL-006run with two independent client processes; separate provider accounts are not required by the storage layer.Retry-After, retry/backoff, collaborator-discovery latency, and a common complexity rubric.Reviewer entry points
Validation
npm run spike:s0:test: 112 passed, 0 failedThe PR remains a draft for reviewer confirmation of the revised evidence and accepted architecture decision.