Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/release-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Release Drift

# Guards against source drift: code merged to main that never gets released, so the
# published @agent-ix/quoin silently lags its source. Fails while main's src/ is ahead
# of the latest release tag. Self-healing — it re-evaluates on tag push (goes green once
# you tag the release) and daily, so it never gets stuck red on an old commit.

on:
push:
branches: [main]
tags: ["*.*.*"]
schedule:
- cron: "0 13 * * *"
workflow_dispatch:

jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history + tags so `git describe` works
- name: Fail if src/ changed since the latest release tag
run: |
set -euo pipefail
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
echo "No release tags yet; nothing to compare against."
exit 0
fi
if git diff --quiet "$LAST_TAG"..HEAD -- src; then
echo "✅ No src/ changes since $LAST_TAG — main is released."
else
echo "::error::src/ has changed since $LAST_TAG. Tag a release (vX.Y.Z) so the published package matches main."
echo "Changed since $LAST_TAG:"
git diff --name-only "$LAST_TAG"..HEAD -- src
exit 1
fi
69 changes: 69 additions & 0 deletions reviews/26-06-26-gap-analysis-pr21.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
id: SR-001
title: "Gap-analysis review of the update-registry + versioning change (Task-010)"
type: SpecReview
analysis: gap-analysis
scope: "spec/plan.md PL-001 Task-010; FR-002; FR-022; spec/matrix.md TM-001"
review_set: subset
relationships:
- target: "ix://agent-ix/quoin/PL-001"
type: "reviews"
- target: "ix://agent-ix/quoin/TM-001"
type: "references"
---

## Summary

Post-implementation gate over the FR-022 self-update registry default plus the
truthful-versioning and release-drift work (PL-001 Task-010, PR #21). The plan is
complete and FR-022 is faithfully implemented and matrix-backed. The gate found one
`high` spec-faithfulness gap (version resolution changed without updating FR-002) and
one `medium` untracked test — **both remediated within PR #21** (see Remediation). One
`low` note (the release-drift CI guard) is accepted as out-of-scope process tooling.

## Verdict

**FAIL at review time → remediated to PASS in PR #21.** FND-001 (high) and FND-002
(medium) are fixed in the same PR; FND-003 (low) is accepted as out-of-scope. The
Findings table below records the gate's original findings for the audit trail.

## Remediation (applied in PR #21)

- **FND-001:** FR-002 Description, Behavior, and a new **FR-002-AC-4** now specify
"prefer the build-time baked `git describe` version; fall back to `package.json`."
- **FND-002:** `tests/version.test.ts` added to the FR-002 Test Matrix row.
- **FND-003:** accepted — the release-drift guard is CI/release tooling, intentionally
not modeled as a product requirement.

## Findings

| ID | Severity | Summary | Refs |
| ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
| FND-001 | high | `packageVersion()` now prefers a build-time baked `git describe` value; FR-002 mandates reading from `package.json` and never describes baked/drift versioning — spec-faithfulness gap + underspecified feature | FR-002 |
| FND-002 | medium | `tests/version.test.ts` ("resolveVersion") is not registered in the Test Matrix FR-002 row — untracked test | TM-001 |
| FND-003 | low | `.github/workflows/release-drift.yml` CI guard has no owning StR/US/FR/NFR (release-process tooling) | - |

## Coverage

- **Plan completion (Step 1): PASS.** PL-001 Task-001…Task-010 are all `Done`; no
blocked/incomplete tasks or stale checkboxes.
- **Matrix verification (Step 2): partial.** FR-022 is backed by `update.test.ts`
(4 cases; names match the matrix row, incl. the new public-npm default case). quoin
maps requirement→test by `file :: "test name"` rather than `FR-xxx-AC-x` code tags, so
"backing" = a named test exists. Gap: `tests/version.test.ts` exists but is absent from
the matrix (FND-002).
- **Underspecified code (Step 3):** the baked-version path (`resolveVersion`,
`__QUOIN_VERSION__`, `vite.config.ts` `gitVersion`) changes FR-002 behavior with no
owning/updated requirement (FND-001); the release-drift guard is unowned process tooling
(FND-003). The FR-022 registry default and `DEFAULT_UPDATE_REGISTRY` are correctly owned
by FR-022.
- **Semantic review (Step 4): not run** (optional; pending user opt-in).

### Remediation to clear FAIL → PASS

1. Update **FR-002** (Description/Behavior + an AC) to specify: resolve the version from the
build-time baked `git describe` value when present, falling back to `package.json`;
include the new test in `update`/`version` AC verification.
2. Add `tests/version.test.ts` to the **FR-002** matrix row.
3. Optionally introduce an NFR (or note) covering the release-drift guard, or record it as
intentionally out of spec scope.
34 changes: 20 additions & 14 deletions spec/functional/FR-002-print-package-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ relationships:

## Description

The CLI SHALL print its own package version — read from the package's
`package.json` — in response to the `version` command, the `--version` flag, or
the `-v` flag, and SHALL raise an error when the package version is missing or
not a string.
The CLI SHALL print its own version in response to the `version` command, the
`--version` flag, or the `-v` flag. The version SHALL be the value baked at build
time from `git describe` — a bare tag for a clean release (e.g. `0.5.2`), a
drift-revealing suffix otherwise (e.g. `0.5.1-3-gabc123-dirty`) — and SHALL fall
back to the package's `package.json` `version` field when no build-time value is
present (dev, test, or no-git builds), raising an error when that fallback version
is missing or not a string.

## Inputs

Expand All @@ -27,19 +30,22 @@ not a string.

## Behavior

- The CLI SHALL resolve its `package.json` relative to the installed package and
read the `version` field.
- The CLI SHALL print that version and return before any other dispatch.
- The CLI SHALL raise an explicit error when the `version` field is absent or is
not a string.
- The CLI SHALL prefer the build-time baked version (`git describe --tags --dirty`,
leading `v` stripped) when it is a non-empty string.
- The CLI SHALL otherwise resolve its `package.json` relative to the installed
package and read the `version` field.
- The CLI SHALL print the resolved version and return before any other dispatch.
- The CLI SHALL raise an explicit error when, on the fallback path, the `version`
field is absent or is not a string.

## Acceptance Criteria

| ID | Criteria | Verification |
| ----------- | --------------------------------------------------------------- | ----------------------------------- |
| FR-002-AC-1 | `version`, `--version`, and `-v` each print the package version | Test (cli.test.ts, scripts.test.ts) |
| FR-002-AC-2 | The reported value is a non-empty string | Test (cli.test.ts) |
| FR-002-AC-3 | A missing or non-string version field raises an error | Test (cli-version-missing.test.ts) |
| ID | Criteria | Verification |
| ----------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------- |
| FR-002-AC-1 | `version`, `--version`, and `-v` each print the package version | Test (cli.test.ts, scripts.test.ts) |
| FR-002-AC-2 | The reported value is a non-empty string | Test (cli.test.ts) |
| FR-002-AC-3 | A missing or non-string version field raises an error | Test (cli-version-missing.test.ts) |
| FR-002-AC-4 | A non-empty baked version is reported verbatim; an empty one falls back to the `package.json` version | Test (version.test.ts) |

## Dependencies

Expand Down
17 changes: 9 additions & 8 deletions spec/functional/FR-022-self-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ published `@agent-ix/quoin` by delegating to `ix-cli-core`'s self-update
(querying the registry, comparing against the running version, and installing
globally), supporting a `--check` mode that reports availability without
installing and a `--registry <url>` option that selects the registry; with no
`--registry`, the ambient npm configuration resolves the package.
`--registry`, the command defaults to the public npm registry
(`https://registry.npmjs.org/`), where `@agent-ix/quoin` is published.

## Inputs

Expand All @@ -34,16 +35,16 @@ installing and a `--registry <url>` option that selects the registry; with no
package coordinates and the running version (see
[FR-002](./FR-002-print-package-version.md)).
- The CLI SHALL report availability without installing when `--check` is given.
- The CLI SHALL pass a `--registry` value through, and SHALL otherwise let the
ambient npm configuration resolve the package.
- The CLI SHALL pass a `--registry` value through, and SHALL otherwise default to
the public npm registry (`https://registry.npmjs.org/`).

## Acceptance Criteria

| ID | Criteria | Verification |
| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- |
| FR-022-AC-1 | `update` delegates to the self-update with `@agent-ix/quoin`, the running version, and the `quoin update` header | Test (update.test.ts) |
| FR-022-AC-2 | `--check` is passed through to report availability without installing | Test (update.test.ts) |
| FR-022-AC-3 | `--registry <url>` is passed through, and its absence leaves resolution to the ambient npm config | Test (update.test.ts) |
| ID | Criteria | Verification |
| ----------- | --------------------------------------------------------------------------------------------------------------------- | --------------------- |
| FR-022-AC-1 | `update` delegates to the self-update with `@agent-ix/quoin`, the running version, and the `quoin update` header | Test (update.test.ts) |
| FR-022-AC-2 | `--check` is passed through to report availability without installing | Test (update.test.ts) |
| FR-022-AC-3 | `--registry <url>` is passed through; its absence defaults to the public npm registry (`https://registry.npmjs.org/`) | Test (update.test.ts) |

## Dependencies

Expand Down
4 changes: 2 additions & 2 deletions spec/matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Coverage is mapped requirement → test as `file :: "test name"`:
| Requirement | Coverage | Test (file :: name) |
| ----------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| FR-001 | ✅ Covered | `cli.test.ts` :: "long flag with = and following-value form both work"; :: "boolean long flag (no value) is honored"; :: "subcommand is only captured for catalog/plugin; others become positionals" |
| FR-002 | ✅ Covered | `cli.test.ts` :: "--version, -v, and the version command all print the package version"; :: "returns a non-empty version string"; `cli-version-missing.test.ts` :: "throws when package.json has no string version" |
| FR-002 | ✅ Covered | `cli.test.ts` :: "--version, -v, and the version command all print the package version"; :: "returns a non-empty version string"; `cli-version-missing.test.ts` :: "throws when package.json has no string version"; `version.test.ts` :: "returns the baked version verbatim when present"; :: "falls back to the package.json version when nothing is baked in" |
| FR-003 | ✅ Covered | `cli.test.ts` :: "no command prints the top-level usage"; :: "--help and -h print command help"; :: "--help on an unrecognized command falls back to the top-level usage"; :: "help for a spec-flow command prints the workflow usage" |
| FR-004 | ✅ Covered | `cli.test.ts` :: "falls back to IX_HOME when --config-root is omitted, and prints 'unknown' for a versionless module"; :: "--no-project-config short-circuits project config root" |
| FR-005 | ✅ Covered | `cli.test.ts` :: "throws on an unknown command"; :: "unknown catalog subcommand throws"; :: "unknown plugin subcommand throws" |
Expand All @@ -54,7 +54,7 @@ Coverage is mapped requirement → test as `file :: "test name"`:
| FR-019 | ✅ Covered | `plugins.test.ts` :: "install adds a plugin from a path source"; :: "installs, lists, then removes a plugin and its target dir + registry entry"; readModuleName suite ("reads the name from a top-level manifest.yaml"…); `cli.test.ts` :: "install without a source throws"; :: "remove without a name throws"; :: "remove deletes a plugin and prints confirmation" |
| FR-020 | ✅ Covered | `flows.test.ts` :: "lists the bundled spec flows"; :: "throws for an unknown flow name"; `flows-notfound.test.ts` :: "throws when no candidate root contains the skill" |
| FR-021 | ✅ Covered | `flows.test.ts` :: "resolves when ix-flow exits 0; builds id/json/target args"; :: "matrix runs the flow and propagates a non-zero exit code"; :: "defaults exit code to 1 when ix-flow is killed by a signal (null code)"; :: "rejects when ix-flow cannot be spawned (PATH has no ix-flow)"; `cli.test.ts` :: "review with --target/--json/--id runs the flow (ix-flow exit 0)" |
| FR-022 | ✅ Covered | `update.test.ts` :: "delegates to runSelfUpdate with quoin's package coordinates" (asserts packageName `@agent-ix/quoin`, currentVersion, header "quoin update", registry undefined → ambient config, check false); :: "passes --check through"; :: "passes a custom --registry through" |
| FR-022 | ✅ Covered | `update.test.ts` :: "delegates to runSelfUpdate with quoin's package coordinates" (asserts packageName `@agent-ix/quoin`, currentVersion, header "quoin update", registry defaults to `https://registry.npmjs.org/`, check false); :: "passes --check through"; :: "passes a custom --registry through"; :: "defaults the registry to public npm when no --registry is given" |

| FR-023 | ✅ Covered | `cli.test.ts` :: "falls back to IX_HOME when --config-root is omitted, and prints 'unknown' for a versionless module"; `catalog.test.ts` :: "includes QUOIN_MODULE_PATHS entries and installed module dirs"; `flows.test.ts` :: "resolves when ix-flow exits 0; builds id/json/target args" (IX_SPEC_WORKFLOWS_ROOT) |

Expand Down
1 change: 1 addition & 0 deletions spec/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ type: Plan
| Task-007 | Add use cases for authoring, validation, plugins, and workflows. | Done |
| Task-008 | Add an eval matrix with latency/token/tool-call metrics. | Done |
| Task-009 | Build executable eval harness and fixture repos. | Done |
| Task-010 | Default `update` to public npm (FR-022) when no `--registry`. | Done |
Loading
Loading