Skip to content

Assay 0.14.0

Latest

Choose a tag to compare

@X-T-E-R X-T-E-R released this 27 Aug 22:33
· 4 commits to main since this release

Assay 0.14.0

Assay 0.14.0 removes the immutability ceremony from the evidence loop, gives
every Source one home reachable from any workspace, prunes Source adoption to a
traceability record, and teaches the CLI to explain its own semantics. The
workspace envelope is 0.14.0+s4+l8: it breaks record formats on purpose, and
assay update migrates a 0.13.0 workspace in place; there are no compatibility
shims behind it.

This note covers the full change set since v0.13.0.

Breaking

  • The workspace envelope is now 0.14.0+s4+l8. A 0.13.0+s4+l8 workspace is
    refused by every command except assay update, which migrates it and says what
    it rewrote. Anything older still requires the external cutover tool. The refusal
    for a migratable workspace now names assay update and carries an
    assay-update:<observed>-><required> locator instead of the cutover locator.
  • source add --mode living|frozen and source add --capture checkout|archive
    are gone, along with the living, frozen, and archive concepts. A source's
    shape follows the material: a Git repository or URL is checkout-backed, and
    anything else is copied once into content/. Scripts passing either flag will
    fail on an unknown option.
  • source.yaml replaces mode and default_capture_mode with a single
    content_mode of checkout or copy. Both retired fields are rejected on read.
  • Source observations are now cheap append records: {observation_id, observed_on, lineage_id, source_path, previous_observation, kind, change_class, note, advisories} plus vcs for a Git source and capture for an observation that
    preserved bytes. The fingerprint, manifest, capture_mode, materials_path,
    checkout_path, and capture_path fields are gone, and a record still carrying
    them is rejected on read. No default path computes a sha256-tree any more.
  • Per-observation manifests under sources/<alias>/manifests/ are no longer
    written or read. A capture keeps its integrity manifest beside its bytes, at
    captures/<id>/manifest.json.
  • source sync and source switch apply only to checkout-backed sources. Calling
    either on copied content is a teaching error that names source import and
    source capture instead.
  • sync and switch no longer refuse a dirty checkout, an unrecorded local
    commit, or checkout bytes that differ from the last observation. They proceed and
    record an observed with local modifications advisory on the observation. Refresh
    is non-destructive: git fetch plus merge --ff-only, never reset --hard or
    clean -fd. An upstream that cannot fast-forward is recorded and left alone.
    Byte-loss protection is delegated to Git, which already declines to overwrite
    uncommitted work.
  • SourceObservationResolution returns contentMode, contentPath, and
    capturePath in place of manifestFile, materialsPath as observation state,
    and checkoutPath.
  • SourceStatusEntry returns contentMode, latestAdvisories, and captures in
    place of mode and captureMode. assay status counts sources as checkouts
    and copies rather than living and frozen.
  • assay source add no longer prints a Manifest: line; it prints Checkout: or
    Content: depending on the content mode.
  • The frozen source alias is no longer reserved, since there is no sources/frozen
    namespace to protect.
  • The Source adoption command surface is take, list, show, remove. The
    register, update, status, inspect, evidence add, verify, decide,
    history, and rollback record subcommands are gone, along with the whole
    inspection, evidence, decision, and rollback workflow behind them: its storage,
    its schemas, the SOURCE_ADOPTION_POLICY_BLOCKED, SOURCE_ADOPTION_STALE, and
    SOURCE_ADOPTION_BUSY error codes, and its tests. A single-user workbench
    records decisions in analysis close --exit and in the adoption's note; the
    adoption record's job is traceability — where did this material land — not an
    approval pipeline.
  • A Source adoption is now one mapping: source alias and path, target system and
    path, an optional note, an optional tier-1 identity pin, and mode. An intent
    that moved several paths is several records. The assay.source-adoption/v1
    schema replaces the assay.source-adoption-definition/v1,
    -state/v1, -inspection/v1, -evidence/v1, and -decision/v1 family, and
    each record is a single file at .assay/source-adoptions/<id>.json rather than
    a per-adoption directory. --mode adapt|copy stays, as descriptive metadata for
    upstream-drift review.
  • Per-adoption .lock files are gone. One record is one atomic file write, so
    there is nothing to serialize; adoption writes still pass through the workspace
    mutation gate that fences a conversion.
  • assay status reports Source adoptions as adoptions, systems, and pinned
    in place of targets, acceptedTargets, and draftTargets. FrameworkStatus
    and --json output change with it.
  • assay check reports Source adoption structure only: whether each record parses
    and validates, whether its filename still matches its id, and whether the system
    it names is still registered.

Added

  • Cross-workspace Source references. assay source link <target-workspace> <target-source> [--alias <local-alias>] writes a thin pointer at
    sources/<local-alias>/source.ref.yaml — schema assay.source-reference/v1,
    fields workspace and source, nothing else — so a Source that already has a
    home is reached instead of cloned again. assay source home <alias> says where
    an alias actually lives, for owned and referenced Sources alike, and
    assay source unlink <alias> forgets the local name. See
    docs/source-reference.md.
  • Read operations on a referenced alias always show the relation. source status
    appends ref -> ../shared-research#qiskit to the row; source log,
    source diff, and source sync print Relation: and Home: lines. A
    reference is never displayed as if the workspace owned the material.
  • Write operations through a reference — sync, capture, import, switch —
    land in the home. They print the home's path before starting the work, run
    under the home workspace's mutation coordination rather than the consumer's,
    and add no confirmation gate. --json callers get the same fact from the
    result's reference block. assay status counts references and
    broken references alongside checkouts and copies.
  • Deletion stays asymmetric. unlink removes the pointer file and the directory
    it was alone in, never recursively and never through to the home; a shell that
    somehow holds content keeps it. Aiming unlink at a Source this workspace owns
    is a teaching error, not a delete. Removing an owned Source is only possible in
    its home workspace.
  • link resolves a chain of references at creation time and records the workspace
    that actually owns source.yaml, so runtime resolution is exactly one hop.
    Linking a target that is already linked is a notice rather than a failure;
    passing --alias explicitly creates a second local name for the same home and
    reports the existing one.
  • A reference whose home cannot be reached fails locally. source status lists it
    as broken with the recorded target and the reason, commands on that alias fail
    with an error naming the repair, assay check reports it as a structure finding
    without scanning for the home or repairing it, and every other object in the
    workspace keeps working. A reference shell holding its own checkout/,
    content/, observations/, or captures/ is refused the same way, because a
    pointer plus material is two answers to where the Source lives.
  • A rebuildable clone registry at ~/.assay/clone-registry.json
    (ASSAY_CLONE_REGISTRY overrides the location): entries of normalized origin
    URI, home workspace root, alias, and last_seen. source add, source link,
    and source sync write to it best-effort — a write that fails never affects the
    command. It buys three hints and no decisions: source add of an origin that
    already has a home prints an advisory naming that home and suggesting
    source link, then proceeds; source link <target-source> with the workspace
    omitted links directly when exactly one home verifies and lists the exact paths
    and stops when several do; a broken-reference error appends the registry's
    current location when one verifies. Every read re-verifies the workspace and
    alias and drops stale entries, it stores absolute machine-local paths while ref
    shells prefer relative ones, and it records homes only — never a consumer's
    pointer. Deleting the file loses those three hints and no facts.
  • assay source capture <alias> [--note <text>] preserves a source's current
    bytes at captures/<id>/source with a sha256-tree-v1 integrity manifest. It
    works for either content mode and is the only routine command that hashes a tree.
  • assay source import <alias> <dir-or-archive> [--note <text>] replaces a copied
    source's content. The bytes it is about to replace are captured first, so an
    import adds a record rather than destroying one.
  • Evidence tiers are named in the semantics registry and the bundled skill: tier 0
    is the default record (alias, date, commit when there is one), tier 1 is identity
    (commit and origin, or a tree hash computed on demand for copied content), tier 2
    is an explicit capture. analysis close --exit adopt|reject suggests recording a
    tier-1 pin; it never requires one.
  • assay source adoption remove <adoption> forgets a mapping. It deletes the
    record and nothing else; the material in the target system is the target
    project's and was never Assay's to write.
  • assay source adoption take gained --note <text> for the rationale worth
    re-reading, and records a tier-1 identity pin without being asked: the commit
    and origin for a checkout-backed source, a sha256-tree-v1 content hash for
    copied material. A target path that escapes the registered system through a
    symbolic link is refused, including one that does not exist yet.
  • assay update gained a one-shot record migration from the previous version. It
    runs before the template analysis, reports every record it rewrote, and stamps the
    new version last so a failed step leaves the workspace re-runnable rather than
    stranded. assay update --dry-run on an unmigrated workspace lists what the
    migration would do without touching anything.

Changed

  • Source file listings are computed when something needs them rather than stored
    with every observation. A capture answers from its manifest; anything else is
    hashed at the moment an adoption or a decision asks for a content pin.
  • source diff reads a checkout-backed source's changes straight from Git
    (diff --name-status between the recorded commits) and compares stored capture
    manifests for copied content.
  • assay check verifies that each source has readable content and that every
    capture still has both its bytes and its manifest, instead of requiring a
    fingerprint and a per-observation manifest.
  • assay source log shows each entry's kind, note, and advisories.
  • The Upstream block in assay status offers assay source sync <alias> whenever
    the remote actually moved, including for a checkout that also holds local work.
    It used to withhold the command there because sync would have refused; now
    sync records the local modification and proceeds, so the command is the one
    action worth taking. The summary for local work says the next sync will record
    it instead of telling the reader to preserve or discard it first.
  • Ordinary assay update now uses recoverable compare-and-swap writes for managed
    files and the Assay AGENTS block, refuses occupied exact .new sidecars before
    mutation, reconciles stale managed receipts after crash recovery, and no longer
    recreates deleted Project guide files.
  • assay task list and assay task validate now answer separate questions, and
    the split is what makes list usable on a large Task tree. list is discovery
    plus storage health: it reads each Task's own envelope once, without taking a
    Task lock and without reading any other record, so the findings it still
    reports are the ones it cannot help noticing — an unparseable task.json, an
    id that disagrees with its directory, and one id filed in more than one place.
    validate owns integrity, which means the lineage graph: proving that every
    relation target exists and that no contributes_to / continues /
    supersedes chain forms a cycle requires reading every reachable record, and
    it is now the only command that pays for it. A dangling relation target is
    reported by validate and nowhere else, so a clean list is not a claim about
    the graph. On a 300-Task workspace with a web of relations, assay task list
    went from over two minutes to about 1.5s.
  • assay check reads each Task's envelope and its own contract files without
    walking the relation graph, and no longer repairs a crash-left checkpoint
    transaction as a side effect of being asked for a health report. assay task validate still recovers it.
  • Any subdirectory under tasks/ (or .assay/tasks/ in an overlay) is now a
    legal navigation prefix: tasks/research/deep/task-0007-<slug>/ is the same
    Task as tasks/task-0007-<slug>/. Resolution is recursive and the stable id
    remains the only identity, so moving a Task directory between prefixes by hand
    is a supported operation that nothing needs to be told about — bindings,
    relations, and Roadmap task_refs all reference ids. A prefix carries no
    schema and no metadata and never enters task.json. An emptied prefix is
    invisible rather than a finding, as is a note a reader leaves beside their
    Tasks. Existing flat workspaces need no migration: flat is the trivial case of
    the same resolution.
  • tasks/archive/ stays reserved and flat. A directory inside it that is not an
    archived Task is reported with the repair rather than silently shadowing the
    archive, and archiving a prefixed Task moves it to tasks/archive/<id>/.
    Below a prefix, archive is an ordinary word: tasks/research/archive/ holds
    live Tasks.
  • The same stable id found under two prefixes goes through the existing
    partial-health duplicate machinery, and the conflict message now names every
    path it found instead of assuming a live/archive pair.
  • assay task create still lands in the flat root. There is no flag for filing a
    new Task into a prefix; organizing is a deliberate move afterwards.
  • Test workspace fixtures now live under one run directory named by
    ASSAY_TEST_FIXTURE_ROOT instead of loose in the shared temp directory, so a
    crashed run's leftovers are identifiable and removable in one sweep.

Migration notes

assay update on a 0.13.0 workspace rewrites source records as follows:

  • A living checkout source becomes content_mode: checkout; its checkout/ stays
    where it is.
  • A frozen source, and a living source that used archive capture, becomes
    content_mode: copy. Its content/ is filled from the newest capture on disk,
    or from the original source path when that is still present. If neither is
    available the directory is created empty and the migration says so on that line.
  • An observation that preserved bytes becomes a capture entry, keeping its
    fingerprint as the capture's integrity value; its manifest is copied to
    captures/<id>/manifest.json.
  • Any other observation becomes an add or sync append record. Its retired
    sha256-tree value is written into the note rather than dropped.
  • sources/<alias>/manifests/ is left on disk untouched. No command reads it any
    more; it is safe to delete once the migrated ledger looks right.
  • A manifest zone purpose that 0.13 shipped verbatim is rewritten to the current
    vocabulary, so assay check stops describing sources/ as "Living and frozen
    external evidence" after the concepts behind that sentence were removed. Only
    exact stock strings are rewritten; a purpose someone edited stays theirs.

It rewrites Source adoption records as follows:

  • Each mapping of each adoption becomes its own record. A single-mapping adoption
    keeps its old id; anything else is suffixed with the mapping id.
  • The last committed decision for that mapping's target survives as a sentence in
    the note: last 0.13.0 decision: <outcome> — <reason>. The old title and the
    adoption and mapping it came from are named in the note as well.
  • An accepted baseline's source identity becomes the record's tier-1 pin — the
    commit it accepted, or the tree hash when there was no commit. 0.13 never
    recorded which origin a commit came from, so that stays null rather than being
    guessed. A target that was never decided migrates without a pin, which is what
    it always was.
  • Inspections, evidence records, decision chains, and rollback records are
    dropped. The migration line counts each kind it dropped.
  • .assay/source-adoptions/<adoption-id>/ is left on disk untouched. No command
    reads it any more; it is safe to delete once the migrated records look right.
  • A mapping this step cannot rewrite into a valid record is reported by name and
    left in the old directory rather than written out half-understood.

Fixed

  • A Task or Roadmap mutation on Windows could report failure after it had
    already succeeded. Tidying the shared coordination directory away runs in the
    mutation's finally block, and Windows answers rmdir with EPERM — not
    ENOTEMPTY — while a concurrent participant's claim directory is still in the
    delete-pending state, so best-effort housekeeping threw over a completed
    write. Every failure at that point means "leave it for the next caller", and
    none of them surfaces now.
  • The Task lock protocol treated Windows delete-pending as a protocol violation.
    A waiter reading the current lock owner got EPERM from lstat or realpath
    while the holder was removing owner.json, where POSIX would have said
    ENOENT; the protocol already knew how to retry that exact situation but
    recognized it under only one of its two names. Removals that have to land also
    ride Node's documented Windows retry instead of failing on a transient
    EPERM. Together these were the recurring EPERM on
    coordination/workspace-mutation/owner.json in the Windows test runs.

Removed

  • Ordinary assay update no longer creates or reports retained timestamp backups;
    existing .assay/backups/ content is left untouched.