mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
Compare commits
60 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
350faa9ff5 | ||
|
|
dfae7911cc | ||
|
|
3851fea93e | ||
|
|
871101addc | ||
|
|
e04abf96a6 | ||
|
|
eee4dd9a2a | ||
|
|
1cdcd6ec60 | ||
|
|
6ab3feb35f | ||
|
|
0d9118ada2 | ||
|
|
7abe012f45 | ||
|
|
93ea9577d2 | ||
|
|
7cc8f253c7 | ||
|
|
49e25838e1 | ||
|
|
415f4077b9 | ||
|
|
c9db917422 | ||
|
|
d5428baec0 | ||
|
|
25be48454b | ||
|
|
2fa9b2ea89 | ||
|
|
3a022ddc44 | ||
|
|
dcdcb0f65d | ||
|
|
5493c022cd | ||
|
|
6e3b8bfe79 | ||
|
|
619d1bae16 | ||
|
|
87d203d2f5 | ||
|
|
ce9bc5c750 | ||
|
|
72ace38440 | ||
|
|
97ac04b968 | ||
|
|
2cbfd3b215 | ||
|
|
8ca792ad87 | ||
|
|
8a294be775 | ||
|
|
392e65d551 | ||
|
|
a38d42c005 | ||
|
|
36e079e83c | ||
|
|
c6fca49cf7 | ||
|
|
3bcf48fb5f | ||
|
|
454850db9e | ||
|
|
7fd9fd1360 | ||
|
|
f5e361ab8f | ||
|
|
06e1343958 | ||
|
|
0c1f5b35f1 | ||
|
|
0c0764c449 | ||
|
|
5a1e0d0d1a | ||
|
|
06d7fa61f8 | ||
|
|
14b6d591a5 | ||
|
|
2b28b51a73 | ||
|
|
2912e116c0 | ||
|
|
91623dba84 | ||
|
|
542d6dd760 | ||
|
|
2a581a451a | ||
|
|
93e946693d | ||
|
|
bd03e2e17c | ||
|
|
b1367aa33c | ||
|
|
9c533c4345 | ||
|
|
f3abf3642e | ||
|
|
61bf2163b5 | ||
|
|
e24b75cf3b | ||
|
|
1faff783ff | ||
|
|
eb12b67921 | ||
|
|
e7ae87f6d2 | ||
|
|
77a67f3e2d |
303 changed files with 18152 additions and 7930 deletions
|
|
@ -7,6 +7,11 @@ node_modules
|
|||
server/dist
|
||||
web/dist
|
||||
shared/dist
|
||||
desktop/.desktop-release
|
||||
desktop/release
|
||||
.veritas-desktop-dev
|
||||
playwright-report
|
||||
test-results
|
||||
|
||||
# Git
|
||||
.git
|
||||
|
|
|
|||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -28,7 +28,7 @@ Describe the tests you ran to verify your changes. Provide instructions so revie
|
|||
**Verification tier:**
|
||||
|
||||
- [ ] Documentation or static checks only
|
||||
- [ ] Focused changed-package tests
|
||||
- [ ] Explicit focused diagnostic (manual workflow dispatch)
|
||||
- [ ] Full milestone gate (`ci:full`, critical security, integration, or release)
|
||||
|
||||
**Why this tier is sufficient:** Explain the changed behavior, covered failure
|
||||
|
|
@ -46,7 +46,7 @@ modes, and why broader gates are or are not required.
|
|||
- [ ] Separable follow-up work is linked instead of folded into this PR
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] I have added or updated coverage for the next declared test milestone
|
||||
- [ ] I have updated the documentation accordingly
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] Any breaking changes have been documented in the PR description
|
||||
|
|
|
|||
224
.github/workflows/ci.yml
vendored
224
.github/workflows/ci.yml
vendored
|
|
@ -13,7 +13,7 @@ on:
|
|||
test_scope:
|
||||
description: Unit-test verification tier
|
||||
required: true
|
||||
default: focused
|
||||
default: full
|
||||
type: choice
|
||||
options:
|
||||
- focused
|
||||
|
|
@ -24,10 +24,7 @@ on:
|
|||
type: string
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
checks: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: >-
|
||||
|
|
@ -60,118 +57,39 @@ jobs:
|
|||
outputs:
|
||||
scope: ${{ steps.scope.outputs.scope }}
|
||||
packages: ${{ steps.scope.outputs.packages }}
|
||||
base_sha: ${{ steps.scope.outputs.base_sha }}
|
||||
diff_range: ${{ steps.scope.outputs.diff_range }}
|
||||
reason: ${{ steps.scope.outputs.reason }}
|
||||
coverage_packages: ${{ steps.scope.outputs.coverage_packages }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Verify CI scope controls
|
||||
run: >-
|
||||
node --test
|
||||
scripts/check-actions-pinned.test.mjs
|
||||
scripts/check-delivery-cadence.test.mjs
|
||||
scripts/check-security-gates.test.mjs
|
||||
scripts/check-tracked-ignore.test.mjs
|
||||
scripts/select-ci-test-scope.test.mjs
|
||||
scripts/verify-full-suite-job-evidence.test.mjs
|
||||
|
||||
- name: Guard delivery cadence
|
||||
run: node scripts/check-delivery-cadence.mjs
|
||||
|
||||
- name: Find reviewed full-suite evidence
|
||||
id: reviewed_full
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
- name: Guard immutable GitHub Actions references
|
||||
run: node scripts/check-actions-pinned.mjs
|
||||
|
||||
reviewed_full=false
|
||||
reviewed_pr=
|
||||
reviewed_head=
|
||||
reviewed_mode=
|
||||
associated_prs="$(
|
||||
gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
"repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" \
|
||||
2>/dev/null ||
|
||||
printf '[]'
|
||||
)"
|
||||
merged_pr="$(
|
||||
jq -c \
|
||||
--arg merge_sha "$GITHUB_SHA" \
|
||||
'[.[] | select(
|
||||
.merged_at != null and
|
||||
.base.ref == "main" and
|
||||
.merge_commit_sha == $merge_sha
|
||||
)] | first // empty' \
|
||||
<<<"$associated_prs"
|
||||
)"
|
||||
- name: Guard continuous security gates
|
||||
run: node scripts/check-security-gates.mjs
|
||||
|
||||
if [[ -n "$merged_pr" ]]; then
|
||||
reviewed_pr="$(jq -r '.number' <<<"$merged_pr")"
|
||||
reviewed_head="$(jq -r '.head.sha' <<<"$merged_pr")"
|
||||
check_runs="$(
|
||||
gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
"repos/${GITHUB_REPOSITORY}/commits/${reviewed_head}/check-runs?per_page=100" \
|
||||
2>/dev/null ||
|
||||
printf '{"check_runs":[]}'
|
||||
)"
|
||||
|
||||
full_suite_check_id="$(
|
||||
jq -r '
|
||||
[
|
||||
.check_runs[] |
|
||||
select(
|
||||
.name == "Workspace Unit Tests" and
|
||||
.status == "completed" and
|
||||
.conclusion == "success" and
|
||||
.app.slug == "github-actions"
|
||||
)
|
||||
] |
|
||||
sort_by(.completed_at) |
|
||||
last |
|
||||
.id // empty
|
||||
' <<<"$check_runs"
|
||||
)"
|
||||
full_suite_job="$(
|
||||
if [[ -n "$full_suite_check_id" ]]; then
|
||||
gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
"repos/${GITHUB_REPOSITORY}/actions/jobs/${full_suite_check_id}" \
|
||||
2>/dev/null ||
|
||||
printf '{}'
|
||||
else
|
||||
printf '{}'
|
||||
fi
|
||||
)"
|
||||
|
||||
if node scripts/verify-full-suite-job-evidence.mjs <<<"$full_suite_job"; then
|
||||
if git cat-file -e "${reviewed_head}^{commit}" 2>/dev/null ||
|
||||
git fetch --no-tags origin "$reviewed_head"; then
|
||||
if git merge-base --is-ancestor "$reviewed_head" "$GITHUB_SHA"; then
|
||||
reviewed_full=true
|
||||
reviewed_mode=ancestor
|
||||
elif [[ "$(git rev-parse "${reviewed_head}^{tree}")" == \
|
||||
"$(git rev-parse "${GITHUB_SHA}^{tree}")" ]]; then
|
||||
reviewed_full=true
|
||||
reviewed_mode=identical-tree
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
{
|
||||
echo "reviewed_full=$reviewed_full"
|
||||
echo "reviewed_pr=$reviewed_pr"
|
||||
echo "reviewed_head=$reviewed_head"
|
||||
echo "reviewed_mode=$reviewed_mode"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Reject tracked files covered by ignore rules
|
||||
run: node scripts/check-tracked-ignore.mjs
|
||||
|
||||
- name: Select verification tier
|
||||
id: scope
|
||||
|
|
@ -180,9 +98,6 @@ jobs:
|
|||
CI_EVENT_NAME: ${{ github.event_name }}
|
||||
CI_MANUAL_SCOPE: ${{ inputs.test_scope || '' }}
|
||||
CI_PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
|
||||
CI_REVIEWED_FULL: ${{ steps.reviewed_full.outputs.reviewed_full || 'false' }}
|
||||
CI_REVIEWED_PR: ${{ steps.reviewed_full.outputs.reviewed_pr || '' }}
|
||||
CI_REVIEWED_MODE: ${{ steps.reviewed_full.outputs.reviewed_mode || '' }}
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha || '' }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || '' }}
|
||||
PUSH_BEFORE_SHA: ${{ github.event.before || '' }}
|
||||
|
|
@ -236,11 +151,11 @@ jobs:
|
|||
github.event.label.name == 'ci:full'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
|
@ -251,6 +166,9 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Check service filesystem boundary
|
||||
run: pnpm check:service-filesystem-boundary
|
||||
|
||||
- name: Build shared (dependency for typecheck)
|
||||
run: pnpm --filter @veritas-kanban/shared build
|
||||
|
||||
|
|
@ -298,15 +216,15 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
if: needs.select-tests.outputs.scope == 'focused'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
if: needs.select-tests.outputs.scope == 'focused'
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
if: needs.select-tests.outputs.scope == 'focused'
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
|
@ -371,6 +289,7 @@ jobs:
|
|||
|
||||
pnpm --filter "$package_filter" exec vitest related \
|
||||
--run \
|
||||
--maxWorkers=4 \
|
||||
--passWithNoTests \
|
||||
"${extra_args[@]}" \
|
||||
"${related_files[@]}"
|
||||
|
|
@ -425,13 +344,13 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
if: needs.select-tests.outputs.scope == 'full'
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
if: needs.select-tests.outputs.scope == 'full'
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
if: needs.select-tests.outputs.scope == 'full'
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
|
@ -476,6 +395,8 @@ jobs:
|
|||
echo "- Selection reason: $SELECTION_REASON"
|
||||
echo "- Workflow checkout SHA: \`$GITHUB_SHA\`"
|
||||
echo "- Current job status: \`$CURRENT_JOB_STATUS\`"
|
||||
echo "- Unit-test workspaces: \`server, web, cli, mcp\`"
|
||||
echo "- Workspace workers: \`4 maximum per Vitest project\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Record full-tier skip
|
||||
|
|
@ -491,6 +412,78 @@ jobs:
|
|||
echo "- Selection reason: $SELECTION_REASON"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# ─── Critical-path Coverage ─────────────────────────────────────
|
||||
critical-path-coverage:
|
||||
name: Critical Path Coverage
|
||||
needs: select-tests
|
||||
if: >-
|
||||
always() &&
|
||||
(
|
||||
github.event_name != 'pull_request' ||
|
||||
!contains(fromJSON('["labeled","unlabeled"]'), github.event.action) ||
|
||||
github.event.label.name == 'ci:full'
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Require a successful scope decision
|
||||
env:
|
||||
SELECTOR_RESULT: ${{ needs.select-tests.result }}
|
||||
run: |
|
||||
if [[ "$SELECTOR_RESULT" != "success" ]]; then
|
||||
echo "::error::Select Test Scope did not complete successfully"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
if: needs.select-tests.outputs.coverage_packages != ''
|
||||
with:
|
||||
# Policy downgrade and changed-critical-file checks compare against the event base SHA.
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
if: needs.select-tests.outputs.coverage_packages != ''
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
if: needs.select-tests.outputs.coverage_packages != ''
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
if: needs.select-tests.outputs.coverage_packages != ''
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Verify coverage policy
|
||||
if: needs.select-tests.outputs.coverage_packages != ''
|
||||
env:
|
||||
COVERAGE_BASE_REF: ${{ needs.select-tests.outputs.base_sha }}
|
||||
run: pnpm check:coverage-policy
|
||||
|
||||
- name: Measure and ratchet critical paths
|
||||
if: needs.select-tests.outputs.coverage_packages != ''
|
||||
env:
|
||||
COVERAGE_PACKAGES: ${{ needs.select-tests.outputs.coverage_packages }}
|
||||
COVERAGE_BASE_REF: ${{ needs.select-tests.outputs.base_sha }}
|
||||
run: pnpm test:coverage --packages "$COVERAGE_PACKAGES"
|
||||
|
||||
- name: Upload coverage reports
|
||||
if: always() && needs.select-tests.outputs.coverage_packages != ''
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: critical-path-coverage-${{ github.sha }}
|
||||
path: coverage/
|
||||
if-no-files-found: warn
|
||||
retention-days: 14
|
||||
|
||||
- name: Record coverage skip
|
||||
if: needs.select-tests.outputs.coverage_packages == ''
|
||||
run: |
|
||||
{
|
||||
echo "### Critical-path coverage ratchets"
|
||||
echo
|
||||
echo "No governed critical-path package changed in this verification scope."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# ─── Build ───────────────────────────────────────────────────────
|
||||
build:
|
||||
name: Build
|
||||
|
|
@ -500,11 +493,11 @@ jobs:
|
|||
github.event.label.name == 'ci:full'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
|
@ -515,6 +508,9 @@ jobs:
|
|||
- name: Build shared (dependency for all builds)
|
||||
run: pnpm --filter @veritas-kanban/shared build
|
||||
|
||||
- name: Verify native Vite config loading
|
||||
run: pnpm check:vite-native-config
|
||||
|
||||
- name: Build all packages
|
||||
run: pnpm build
|
||||
|
||||
|
|
@ -555,11 +551,11 @@ jobs:
|
|||
github.event.label.name == 'ci:full'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
|
|
|||
85
.github/workflows/desktop-artifacts.yml
vendored
85
.github/workflows/desktop-artifacts.yml
vendored
|
|
@ -1,31 +1,9 @@
|
|||
name: Desktop Artifacts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'desktop/**'
|
||||
- 'server/**'
|
||||
- 'web/**'
|
||||
- 'shared/**'
|
||||
- 'docs/DESKTOP-RELEASE.md'
|
||||
- 'scripts/desktop-after-pack.mjs'
|
||||
- 'scripts/prepare-desktop-release.mjs'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- '.github/workflows/desktop-artifacts.yml'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'desktop/**'
|
||||
- 'docs/DESKTOP-RELEASE.md'
|
||||
- 'scripts/desktop-after-pack.mjs'
|
||||
- 'scripts/prepare-desktop-release.mjs'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- '.github/workflows/desktop-artifacts.yml'
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
|
@ -38,13 +16,25 @@ env:
|
|||
jobs:
|
||||
mac-unsigned:
|
||||
name: Unsigned macOS Artifact
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'ci:full')
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Record milestone selection
|
||||
run: |
|
||||
{
|
||||
echo "### Unsigned macOS artifact milestone"
|
||||
echo
|
||||
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
|
||||
echo "- Reason: explicit \`ci:full\` or manual milestone"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
|
@ -65,7 +55,7 @@ jobs:
|
|||
run: node ./node_modules/electron-builder/cli.js --mac dmg zip --publish never --config.mac.identity=null --config.mac.notarize=false
|
||||
|
||||
- name: Upload desktop artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: veritas-kanban-mac-unsigned
|
||||
path: |
|
||||
|
|
@ -78,13 +68,25 @@ jobs:
|
|||
|
||||
linux-unsigned:
|
||||
name: Unsigned Linux Artifacts
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'ci:full')
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Record milestone selection
|
||||
run: |
|
||||
{
|
||||
echo "### Unsigned Linux artifact milestone"
|
||||
echo
|
||||
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
|
||||
echo "- Reason: explicit \`ci:full\` or manual milestone"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
|
@ -108,7 +110,7 @@ jobs:
|
|||
run: node ./node_modules/electron-builder/cli.js --linux AppImage deb rpm --x64 --publish never
|
||||
|
||||
- name: Upload Linux preview desktop artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: veritas-kanban-linux-unsigned
|
||||
path: |
|
||||
|
|
@ -122,13 +124,26 @@ jobs:
|
|||
|
||||
windows-unsigned:
|
||||
name: Unsigned Windows Artifacts
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'ci:full')
|
||||
runs-on: windows-2025
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Record milestone selection
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "### Unsigned Windows artifact milestone"
|
||||
echo
|
||||
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
|
||||
echo "- Reason: explicit \`ci:full\` or manual milestone"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
|
@ -149,7 +164,7 @@ jobs:
|
|||
run: node ./node_modules/electron-builder/cli.js --win nsis zip --x64 --publish never
|
||||
|
||||
- name: Upload Windows preview desktop artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: veritas-kanban-windows-unsigned
|
||||
path: |
|
||||
|
|
|
|||
6
.github/workflows/desktop-release.yml
vendored
6
.github/workflows/desktop-release.yml
vendored
|
|
@ -32,11 +32,11 @@ jobs:
|
|||
name: Signed and Notarized macOS Artifact
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
|
|
|||
39
.github/workflows/docker-image.yml
vendored
Normal file
39
.github/workflows/docker-image.yml
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
name: Docker Image Contract
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docker-image-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
contract:
|
||||
name: Build, Size, and Runtime Contract
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'ci:full')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Record milestone selection
|
||||
run: |
|
||||
{
|
||||
echo "### Docker image milestone"
|
||||
echo
|
||||
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
|
||||
echo "- Reason: explicit \`ci:full\` or manual milestone"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Build production image
|
||||
run: docker build --target production --tag veritas-kanban:contract .
|
||||
|
||||
- name: Enforce image and runtime contract
|
||||
run: node scripts/check-docker-image.mjs veritas-kanban:contract
|
||||
44
.github/workflows/scheduled-qa.yml
vendored
44
.github/workflows/scheduled-qa.yml
vendored
|
|
@ -1,6 +1,9 @@
|
|||
name: Scheduled QA
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
schedule:
|
||||
- cron: '17 8 * * 1'
|
||||
workflow_dispatch:
|
||||
|
|
@ -36,14 +39,26 @@ env:
|
|||
jobs:
|
||||
playwright:
|
||||
name: Playwright E2E
|
||||
if: >-
|
||||
github.event_name != 'pull_request' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'ci:full')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Record milestone selection
|
||||
run: |
|
||||
{
|
||||
echo "### Playwright E2E milestone"
|
||||
echo
|
||||
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
|
||||
echo "- Reason: explicit \`ci:full\`, scheduled, or manual milestone"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
|
@ -70,7 +85,7 @@ jobs:
|
|||
|
||||
- name: Upload Playwright artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-artifacts
|
||||
path: |
|
||||
|
|
@ -81,16 +96,29 @@ jobs:
|
|||
|
||||
k6:
|
||||
name: k6 Load Smoke
|
||||
if: >-
|
||||
github.event_name != 'pull_request' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'ci:full')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
K6_PROFILE: ${{ github.event_name == 'workflow_dispatch' && inputs.load_profile || 'smoke' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Record milestone selection
|
||||
run: |
|
||||
{
|
||||
echo "### k6 milestone"
|
||||
echo
|
||||
echo "- Trigger: \`$GITHUB_EVENT_NAME\`"
|
||||
echo "- Profile: \`$K6_PROFILE\`"
|
||||
echo "- Reason: explicit \`ci:full\`, scheduled, or manual milestone"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
|
@ -165,7 +193,7 @@ jobs:
|
|||
|
||||
- name: Upload k6 artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: k6-artifacts
|
||||
path: |
|
||||
|
|
|
|||
65
.github/workflows/security.yml
vendored
Normal file
65
.github/workflows/security.yml
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
name: Security Gates
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '17 9 * * 3'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
name: CodeQL
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
build-mode: none
|
||||
queries: security-extended
|
||||
- name: Analyze JavaScript and TypeScript
|
||||
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
with:
|
||||
category: '/language:javascript-typescript'
|
||||
|
||||
gitleaks:
|
||||
name: Gitleaks
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
GITLEAKS_VERSION: 8.30.1
|
||||
GITLEAKS_LINUX_X64_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- name: Download verified gitleaks release
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
archive="$RUNNER_TEMP/gitleaks.tar.gz"
|
||||
curl --fail --silent --show-error --location \
|
||||
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||
--output "$archive"
|
||||
echo "${GITLEAKS_LINUX_X64_SHA256} ${archive}" | sha256sum --check --status
|
||||
tar -xzf "$archive" -C "$RUNNER_TEMP" gitleaks
|
||||
- name: Guard security workflow policy
|
||||
run: pnpm check:security-gates
|
||||
- name: Scan reviewed tree and test detection
|
||||
env:
|
||||
GITLEAKS_BIN: ${{ runner.temp }}/gitleaks
|
||||
run: pnpm check:gitleaks
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -44,10 +44,8 @@ tasks/archive/*.md
|
|||
tasks/backlog/*.md
|
||||
tasks/attachments/
|
||||
tasks/archive-attachments/
|
||||
storage/
|
||||
server/storage/
|
||||
!server/src/storage/
|
||||
!server/src/storage/**
|
||||
/storage/
|
||||
/server/storage/
|
||||
.veritas-kanban/*
|
||||
!.veritas-kanban/.gitkeep
|
||||
.veritas-desktop-dev/
|
||||
|
|
@ -112,7 +110,7 @@ tasks/
|
|||
!tasks/
|
||||
!tasks/examples/
|
||||
!tasks/examples/*.md
|
||||
.veritas-kanban/
|
||||
/.veritas-kanban/
|
||||
|
||||
# Local security middleware (not shared)
|
||||
server/src/middleware/external-api-key.ts
|
||||
|
|
|
|||
|
|
@ -1,22 +1,53 @@
|
|||
# Gitleaks False Positives
|
||||
# Updated: 2026-01-29 (post-history-rewrite)
|
||||
# All entries below are placeholder/example/test values, NOT real secrets.
|
||||
# CLI snapshot uses an intentionally synthetic API key in serialized output.
|
||||
cli/src/__tests__/snapshot.test.ts:generic-api-key:220
|
||||
|
||||
# Documentation example: "your-admin-key" placeholder in deployment guide
|
||||
39423f74cf3849684e8de4ebf746156a6be0ea00:docs/DEPLOYMENT.md:curl-auth-header:557
|
||||
# API documentation contains non-functional response examples.
|
||||
docs/API-REFERENCE.md:generic-api-key:991
|
||||
docs/API-WORKFLOWS.md:generic-api-key:1460
|
||||
|
||||
# Documentation example: "dev-admin-key" placeholder in security audit
|
||||
f01a0157f1d1612ab7bea646cf7265ec018e1d9a:docs/SECURITY_AUDIT_2026-01-28.md:curl-auth-header:135
|
||||
# Operator documentation uses placeholders in curl authentication examples.
|
||||
docs/DEPLOYMENT.md:curl-auth-header:926
|
||||
docs/TROUBLESHOOTING.md:curl-auth-header:230
|
||||
docs/TROUBLESHOOTING.md:curl-auth-header:258
|
||||
docs/TROUBLESHOOTING.md:curl-auth-header:261
|
||||
docs/features/prd-driven-development.md:curl-auth-header:95
|
||||
docs/features/prd-driven-development.md:curl-auth-header:775
|
||||
docs/guides/SELF_HOST.md:curl-auth-header:742
|
||||
docs/security.md:curl-auth-header:51
|
||||
docs/security.md:curl-auth-header:58
|
||||
|
||||
# Test fixture: hardcoded test JWT secret (not used in production)
|
||||
f01a0157f1d1612ab7bea646cf7265ec018e1d9a:server/src/__tests__/routes/auth.test.ts:generic-api-key:28
|
||||
f01a0157f1d1612ab7bea646cf7265ec018e1d9a:server/src/__tests__/routes/auth.test.ts:generic-api-key:29
|
||||
f01a0157f1d1612ab7bea646cf7265ec018e1d9a:server/src/__tests__/routes/auth.test.ts:generic-api-key:60
|
||||
# Demo seeding passes the operator-provided key variable to curl.
|
||||
seed-demo-data.sh:curl-auth-header:45
|
||||
|
||||
# .env.example placeholder values ("your-api-key")
|
||||
55c742c2dfd731069505e1baab16bb4c328234bd:server/.env.example:curl-auth-header:43
|
||||
55c742c2dfd731069505e1baab16bb4c328234bd:server/.env.example:curl-auth-header:46
|
||||
# Environment template documents shell-variable authentication examples.
|
||||
server/.env.example:curl-auth-header:127
|
||||
server/.env.example:curl-auth-header:130
|
||||
|
||||
# Documentation placeholder values ("your-api-key")
|
||||
55c742c2dfd731069505e1baab16bb4c328234bd:docs/security.md:curl-auth-header:40
|
||||
55c742c2dfd731069505e1baab16bb4c328234bd:docs/security.md:curl-auth-header:47
|
||||
# Compatibility test verifies redaction of a deliberately synthetic value.
|
||||
server/src/__tests__/buzz-compatibility-service.test.ts:generic-api-key:477
|
||||
|
||||
# Governance trace test verifies Stripe-shaped token redaction.
|
||||
server/src/__tests__/governance-trace-service.test.ts:stripe-access-token:22
|
||||
|
||||
# Log redaction tests require JWT- and Stripe-shaped synthetic fixtures.
|
||||
server/src/__tests__/log-redaction.test.ts:jwt:17
|
||||
server/src/__tests__/log-redaction.test.ts:stripe-access-token:30
|
||||
server/src/__tests__/log-redaction.test.ts:stripe-access-token:31
|
||||
|
||||
# Completion service test verifies JWT-shaped output redaction.
|
||||
server/src/__tests__/provider-completion-service.test.ts:jwt:314
|
||||
|
||||
# Local admission tests use synthetic idempotency keys, not credentials.
|
||||
server/src/__tests__/routes/agents-local-capability.test.ts:generic-api-key:606
|
||||
server/src/__tests__/routes/agents-local-capability.test.ts:generic-api-key:619
|
||||
|
||||
# Authentication route tests require a synthetic JWT signing value.
|
||||
server/src/__tests__/routes/auth.test.ts:generic-api-key:29
|
||||
server/src/__tests__/routes/auth.test.ts:generic-api-key:31
|
||||
server/src/__tests__/routes/auth.test.ts:generic-api-key:80
|
||||
|
||||
# Skill capability test verifies Stripe-shaped token redaction.
|
||||
server/src/__tests__/skill-capability-service.test.ts:stripe-access-token:63
|
||||
|
||||
# Multi-user UI test renders a non-secret token prefix fixture.
|
||||
web/src/__tests__/multi-user-tab.test.tsx:generic-api-key:123
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
pnpm check:security-artifacts
|
||||
pnpm check:actions-pinned
|
||||
pnpm check:tracked-ignore
|
||||
node scripts/check-delivery-cadence.mjs
|
||||
npx lint-staged
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# Pre-commit hooks for veritas-kanban
|
||||
# Install: pip install pre-commit && pre-commit install
|
||||
# Or standalone gitleaks hook (no pre-commit framework needed):
|
||||
# gitleaks protect --staged --verbose
|
||||
# Or scan the reviewed tree without the pre-commit framework:
|
||||
# gitleaks dir . --redact=100
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.21.2
|
||||
rev: 83d9cd684c87d95d656c1458ef04895a7f1cbd8e # v8.30.1
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
|
|
|
|||
67
AGENTS.md
67
AGENTS.md
|
|
@ -6,7 +6,7 @@
|
|||
> Harness-specific supplements (for example `CLAUDE.md`) extend, never duplicate or contradict,
|
||||
> these rules. See `docs/AGENTS-TEMPLATE.md` for the managed-run and external-agent protocols.
|
||||
>
|
||||
> **Version:** 6.1.1
|
||||
> **Version:** 6.1.2
|
||||
> **Freshness policy:** update within two working days of any toolchain or architecture change.
|
||||
> Stale fields (package manager, Node version, provider list, test commands) are caught by
|
||||
> `pnpm check:pnpm-settings` and the smoke-test CI job.
|
||||
|
|
@ -58,9 +58,10 @@ pnpm build
|
|||
pnpm dev
|
||||
|
||||
# Tests
|
||||
pnpm test # Vitest across server, web, mcp, cli
|
||||
pnpm test:unit # Per-workspace tests sequentially
|
||||
pnpm test:e2e # Playwright end-to-end
|
||||
pnpm test # Canonical sequential workspace unit gate
|
||||
pnpm test:unit # Shared build, then server, web, CLI, and MCP
|
||||
pnpm test:coverage # Critical-path V8 coverage, HTML/JSON reports, and ratchets
|
||||
pnpm test:e2e # Playwright end-to-end, zero retries
|
||||
|
||||
# Type check (builds shared first)
|
||||
pnpm typecheck
|
||||
|
|
@ -70,8 +71,15 @@ pnpm lint
|
|||
pnpm lint:fix
|
||||
|
||||
# Smoke checks
|
||||
pnpm check:actions-pinned # Rejects mutable external GitHub Action references
|
||||
pnpm check:pnpm-settings # Validates package manager fields match this file
|
||||
pnpm check:tracked-ignore # Rejects tracked files covered by ignore rules
|
||||
pnpm check:coverage-policy # Validates coverage policy, configs, CI, and regression tests
|
||||
pnpm check:delivery-cadence # Prevents verification and review policy drift
|
||||
pnpm check:security-gates # Validates CodeQL/gitleaks workflow and exact suppressions
|
||||
pnpm check:gitleaks # Scans reviewed tree and proves new-secret detection
|
||||
pnpm check:vite-native-config # Loads web build and test configs with Vite's native loader
|
||||
pnpm check:service-filesystem-boundary # Prevents new direct filesystem imports in services
|
||||
pnpm test:ci-scope # Validates path-aware CI test selection
|
||||
pnpm smoke:cli-mcp # CLI ↔ MCP compatibility smoke test
|
||||
pnpm test:buzz:compatibility # Credential-free composed Buzz release gate
|
||||
|
|
@ -118,9 +126,11 @@ Do not run `npm install`, `yarn`, or `bun install`. If lockfile conflicts arise,
|
|||
- At the 45-minute delivery checkpoint, if the issue is not pull-request ready, stop adding scope
|
||||
and report the concrete cause. Split independent remaining work into linked issues, or continue
|
||||
only when the next step is required to preserve correctness of the current behavior.
|
||||
- During implementation, run the narrowest useful loop: type-check touched packages, lint changed
|
||||
files, and run focused tests for changed behavior and high-risk edges.
|
||||
- Run focused Vitest slices with
|
||||
- During ordinary implementation, use source inspection, changed-file formatting/linting, and
|
||||
touched-package type checking. Do not run workspace unit, coverage, E2E, desktop packaging, or
|
||||
Docker contract tests between implementation PRs.
|
||||
- When a maintainer explicitly declares a focused diagnostic milestone, run the exact Vitest slice
|
||||
once with
|
||||
`pnpm --filter <package> exec vitest run <exact-test-files>`. Do not use
|
||||
`pnpm --filter <package> test -- <test-files>` or
|
||||
`pnpm --filter <package> test -- --run <test-files>`; package wrappers can ignore that file
|
||||
|
|
@ -129,9 +139,11 @@ Do not run `npm install`, `yarn`, or `bun install`. If lockfile conflicts arise,
|
|||
Rerun only the checks affected by the later change.
|
||||
- Use the complete workspace suite once at an explicit integration, critical-security, or release
|
||||
milestone. Pull-request label `ci:full`, scheduled CI, and manual full dispatch are the
|
||||
authoritative broad gates.
|
||||
- Trust `scripts/select-ci-test-scope.mjs` and the `Select Test Scope` job to choose the required
|
||||
CI tier. Do not add broader local gates merely to duplicate CI.
|
||||
authoritative broad gates. Critical coverage, unsigned desktop artifacts, and the Docker image
|
||||
contract run only at those milestones.
|
||||
- Trust `scripts/select-ci-test-scope.mjs` and the `Select Test Scope` job to record the required
|
||||
CI tier. Ordinary pull requests and `main` pushes select no workspace tests. Do not add local
|
||||
test gates merely to duplicate a future milestone.
|
||||
- Do not wait for optional desktop packaging, artifact previews, or release workflows when the
|
||||
change does not touch their product boundary. They are evidence only when declared relevant.
|
||||
- Add enough regression coverage to prove the behavior and its meaningful failure modes. Test
|
||||
|
|
@ -480,23 +492,24 @@ it.
|
|||
|
||||
## File locations quick-reference
|
||||
|
||||
| What | Where |
|
||||
| ---------------- | ------------------------------------- |
|
||||
| API routes | `server/src/routes/` |
|
||||
| Services | `server/src/services/` |
|
||||
| Zod schemas | `server/src/schemas/` |
|
||||
| Storage | `server/src/storage/` |
|
||||
| Server utilities | `server/src/utils/` |
|
||||
| React components | `web/src/components/` |
|
||||
| Zustand stores | `web/src/stores/` |
|
||||
| CLI commands | `cli/src/commands/` |
|
||||
| Shared types | `shared/src/` |
|
||||
| MCP server | `mcp/src/` |
|
||||
| Prompt registry | `prompt-registry/` |
|
||||
| SOPs | `docs/SOP-*.md` |
|
||||
| Agent registry | `.veritas-kanban/agent-registry.json` |
|
||||
| Agent run logs | `.veritas-kanban/logs/` |
|
||||
| Telemetry events | `.veritas-kanban/telemetry/` |
|
||||
| What | Where |
|
||||
| ----------------- | -------------------------------------------------------- |
|
||||
| API routes | `server/src/routes/` |
|
||||
| Services | `server/src/services/` |
|
||||
| Zod schemas | `server/src/schemas/` |
|
||||
| Storage | `server/src/storage/` |
|
||||
| Server utilities | `server/src/utils/` |
|
||||
| Provider adapters | `server/src/services/agent-provider-adapter-registry.ts` |
|
||||
| React components | `web/src/components/` |
|
||||
| Zustand stores | `web/src/stores/` |
|
||||
| CLI commands | `cli/src/commands/` |
|
||||
| Shared types | `shared/src/` |
|
||||
| MCP server | `mcp/src/` |
|
||||
| Prompt registry | `prompt-registry/` |
|
||||
| SOPs | `docs/SOP-*.md` |
|
||||
| Agent registry | `.veritas-kanban/agent-registry.json` |
|
||||
| Agent run logs | `.veritas-kanban/logs/` |
|
||||
| Telemetry events | `.veritas-kanban/telemetry/` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
87
CHANGELOG.md
87
CHANGELOG.md
|
|
@ -7,6 +7,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [6.1.2] - 2026-08-24
|
||||
|
||||
Veritas Kanban 6.1.2 completes the repository-wide reliability, security,
|
||||
storage, provider-runtime, CI, container, and supportability audit tracked in
|
||||
#1174. It is a backward-compatible patch release for 6.1.1.
|
||||
|
||||
### Added
|
||||
|
||||
- Added deterministic, milestone-scoped CI selection so ordinary pull requests
|
||||
keep test, coverage, E2E, desktop artifact, load, and Docker-contract jobs
|
||||
dormant while `ci:full`, scheduled, and manual release milestones run the
|
||||
complete gates (#1172, #1227, #1228).
|
||||
- Added risk-weighted critical-path coverage baselines and ratchets for provider
|
||||
dispatch, attempt lifecycle, authentication, storage, web API/session, CLI,
|
||||
MCP, and desktop trust boundaries (#1169, #1183).
|
||||
- Added continuous CodeQL, dependency, and secret-scanning policy enforcement,
|
||||
immutable GitHub Actions references, and repository guards that prevent those
|
||||
controls from silently regressing (#1167, #1168, #1179, #1180).
|
||||
- Triaged the initial CodeQL baseline, fixed validated request, logging,
|
||||
persisted-key, file-handling, and sandbox-read findings, and documented the
|
||||
evidence-backed disposition of non-exploitable alerts (#1231, #1232-#1235).
|
||||
- Added a production Docker runtime size contract with architecture-specific
|
||||
ceilings, non-root runtime checks, health/auth/SQLite/static-web smoke
|
||||
coverage, and a reduced build context (#1166, #1222).
|
||||
|
||||
### Changed
|
||||
|
||||
- Centralized `DATA_DIR` and `VERITAS_DATA_DIR` resolution, legacy-location
|
||||
discovery, migration, backup, integrity, and Docker-mounted runtime behavior
|
||||
behind the canonical path contract (#1162, #1184).
|
||||
- Restored the service/storage boundary across activity, progress, status
|
||||
history, scheduled deliverables, workflows, broadcasts, conflicts,
|
||||
delegation, ceremony, error analyses, permissions, lifecycle configuration,
|
||||
scheduler, reflection, chat, task, telemetry, and managed-content persistence.
|
||||
File and SQLite implementations retain their existing compatibility,
|
||||
containment, locking, and atomic-write contracts (#1163, #1190-#1220).
|
||||
- Decomposed the provider control path into explicit launch compilation, Codex
|
||||
event interpretation, runtime resolution, completion, attempt mutation, and
|
||||
adapter-registry contracts. Executable providers remain explicit and unknown
|
||||
or mismatched profiles continue to fail closed without an OpenClaw fallback
|
||||
(#1164, #1223-#1230).
|
||||
- Routed frontend JSON, blob, and stream operations through credential-aware API
|
||||
helpers, preserving cross-origin cookie authentication, base paths, and
|
||||
server error envelopes (#1165, #1218).
|
||||
- Removed four verified unused direct dependencies, regenerated the workspace
|
||||
dependency graph with pnpm 11.1.1, and reduced the server lint-warning budget
|
||||
from 600 to 458 without broad suppressions (#1170, #1173, #1217, #1221).
|
||||
- Replaced loader-fragile Vite/Vitest path handling with native ESM-compatible
|
||||
configuration and made root workspace test discovery deterministic (#1171,
|
||||
#1172, #1175, #1177, #1178, #1181).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Eliminated split runtime-state locations and service-layer persistence leaks
|
||||
that could send live data, backups, health checks, or migrations to different
|
||||
roots under custom data-directory configurations (#1162, #1163).
|
||||
- Hardened request rate limits, structured logging, persisted dynamic keys,
|
||||
bounded file reads and writes, and sandbox metadata reads identified by the
|
||||
initial CodeQL baseline (#1231, #1232-#1235).
|
||||
- Integrated coordinated validation hardening for a privately reported input
|
||||
boundary. The repository security advisory was published after supported
|
||||
6.1.2 artifacts were verified and disclosure was approved (#1236,
|
||||
[GHSA-4r99-qpvh-wrqf](https://github.com/BradGroux/veritas-kanban/security/advisories/GHSA-4r99-qpvh-wrqf)).
|
||||
- Corrected recovery-key alphabet generation and WebSocket upgrade header
|
||||
forwarding defects exposed by the final release validation (#1238, #1239).
|
||||
- Serialized complete same-task update and lifecycle operations before their
|
||||
first read, preserving archive/restore invocation order under contention
|
||||
(#1240, #1241).
|
||||
- Rejected digit-prefixed unsafe URI payloads in sanitized HTML while
|
||||
preserving safe relative links (#1242, #1243).
|
||||
|
||||
### Compatibility and operations
|
||||
|
||||
- The public REST API remains `v1`. Package, CLI, MCP, desktop, provider-profile,
|
||||
and configuration contracts remain backward compatible with 6.1.1.
|
||||
- SQLite migrations remain at 30 through 33; upgrading from 6.1.1 does not run a
|
||||
new schema migration. Runtime path normalization may move legacy files into
|
||||
the configured canonical data directory. Keep a complete stopped-writer
|
||||
backup until the upgraded runtime is accepted.
|
||||
- Rollback is restore-first: stop every writer, reinstall the prior signed
|
||||
application only when its data contracts remain compatible, and otherwise
|
||||
restore the complete pre-upgrade workspace. Never copy an older database over
|
||||
a running instance.
|
||||
|
||||
## [6.1.1] - 2026-08-22
|
||||
|
||||
Veritas Kanban 6.1.1 restores reliable Task Detail scrolling after the Mantine
|
||||
|
|
@ -2463,7 +2547,8 @@ Veritas Kanban is an AI-native project management board built for developers and
|
|||
|
||||
_Built by [Digital Meld](https://digitalmeld.io) — AI-driven enterprise automation._
|
||||
|
||||
[unreleased]: https://github.com/BradGroux/veritas-kanban/compare/v6.1.1...HEAD
|
||||
[unreleased]: https://github.com/BradGroux/veritas-kanban/compare/v6.1.2...HEAD
|
||||
[6.1.2]: https://github.com/BradGroux/veritas-kanban/compare/v6.1.1...v6.1.2
|
||||
[6.1.1]: https://github.com/BradGroux/veritas-kanban/compare/v6.1.0...v6.1.1
|
||||
[6.1.0]: https://github.com/BradGroux/veritas-kanban/compare/v6.0.2...v6.1.0
|
||||
[6.0.2]: https://github.com/BradGroux/veritas-kanban/compare/v6.0.1...v6.0.2
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
> Claude-specific lessons and common mistakes caught by previous Claude runs. Do not duplicate
|
||||
> `AGENTS.md` content here.
|
||||
>
|
||||
> **Last updated:** 2026-08-22 (v6.1.1 release freshness)
|
||||
> **Last updated:** 2026-08-24 (v6.1.2 release freshness)
|
||||
> **Freshness check:** Update after mistakes; review monthly.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -73,16 +73,17 @@ veritas-kanban/
|
|||
|
||||
2. Make your changes — write code, add tests, update docs.
|
||||
|
||||
3. Run touched-package type checking, changed-file linting, and focused tests
|
||||
before committing:
|
||||
3. Run touched-package type checking and changed-file linting before
|
||||
committing. Ordinary implementation pull requests do not run workspace
|
||||
tests:
|
||||
|
||||
```bash
|
||||
pnpm --filter @veritas-kanban/server typecheck
|
||||
pnpm exec eslint server/src/path/to/changed.ts
|
||||
pnpm --filter @veritas-kanban/server exec vitest run src/path/to/changed.test.ts
|
||||
```
|
||||
|
||||
Use direct `exec vitest run` invocation for exact-file slices. Do not use
|
||||
At an explicitly declared focused diagnostic milestone, use direct
|
||||
`exec vitest run` invocation for one exact-file slice. Do not use
|
||||
`pnpm --filter <package> test -- <test-files>` or
|
||||
`pnpm --filter <package> test -- --run <test-files>` as a focused command.
|
||||
Package wrappers can ignore that file boundary and expand into the entire
|
||||
|
|
@ -91,7 +92,8 @@ veritas-kanban/
|
|||
Build `@veritas-kanban/shared` first and type-check its known consumers when
|
||||
a shared contract changes. Use `pnpm test` at an explicit integration,
|
||||
critical-security, or release milestone, or when a maintainer explicitly
|
||||
selects the `ci:full` gate.
|
||||
selects the `ci:full` gate. Critical coverage, E2E, desktop packaging, and
|
||||
Docker contracts follow the same milestone boundary.
|
||||
|
||||
4. Commit using [conventional commits](#commit-conventions).
|
||||
|
||||
|
|
@ -109,8 +111,9 @@ This cadence extends the deterministic CI selector delivered in
|
|||
verification effort becomes larger than the changed behavior.
|
||||
- Do not rerun an unchanged passing check after documentation, comments, or
|
||||
formatting-only edits.
|
||||
- Treat `Select Test Scope` as the CI authority. Focused, full, and no-test
|
||||
selections are recorded in the job summary.
|
||||
- Treat `Select Test Scope` as the CI authority. Ordinary pull requests and
|
||||
`main` pushes select no workspace tests; manual focused diagnostics and full
|
||||
milestone selections are recorded in the job summary.
|
||||
- Do not wait for optional desktop artifacts, packaging previews, or release
|
||||
workflows unless the pull request changes that product boundary.
|
||||
- Test the behavior and meaningful failure modes. Do not use raw test count as
|
||||
|
|
@ -129,14 +132,16 @@ can rebase on the exact result.
|
|||
1. Merge first branch to `main`
|
||||
2. Confirm the required GitHub checks for that pull request
|
||||
3. Rebase the next branch on the updated `main`
|
||||
4. Run only the focused checks affected by conflict resolution
|
||||
4. Inspect conflict resolution and run changed-file static checks
|
||||
5. Merge the next branch
|
||||
|
||||
The complete build, workspace suite, integration suite, and applicable E2E or
|
||||
artifact gates run once at the declared milestone. They are not repeated after
|
||||
every unrelated merge.
|
||||
The complete workspace suite, coverage, integration, E2E, desktop artifact,
|
||||
and Docker gates run once at the declared milestone. They are not repeated
|
||||
after every unrelated merge.
|
||||
|
||||
**Why:** Parallel branches often introduce integration issues that are hidden when batch-merging. Sequential merges with testing between each merge catch these immediately.
|
||||
**Why:** Sequential merges keep conflicts attributable without paying the
|
||||
release-certification cost after every independent change. The declared
|
||||
milestone verifies the integrated candidate once.
|
||||
|
||||
### One Agent Per File Rule
|
||||
|
||||
|
|
@ -192,7 +197,7 @@ owner explicitly requires it.
|
|||
Before merging, verify the checks selected for the changed product boundary:
|
||||
|
||||
- [ ] **Selected CI tier:** Every required check started for the pull request is green.
|
||||
- [ ] **Focused local evidence:** Changed behavior and meaningful failure modes are covered.
|
||||
- [ ] **Implementation evidence:** The diff and applicable static checks support the changed behavior.
|
||||
- [ ] **Shared contracts, when changed:** New types are exported and known consumers type-check.
|
||||
- [ ] **Configuration, when changed:** Ports, URLs, timeouts, environment variables, CSP, and CORS behave in the affected modes.
|
||||
- [ ] **Frontend integration, when changed:** HTTP calls use shared helpers and location-sensitive behavior avoids hardcoded hosts.
|
||||
|
|
@ -211,8 +216,9 @@ Before merging, verify the checks selected for the changed product boundary:
|
|||
|
||||
### Testing Requirements
|
||||
|
||||
Run browser or API smoke tests only when the change affects that product
|
||||
boundary. Choose the smallest runtime check that proves the behavior:
|
||||
Run browser or API smoke tests only at an explicit integration or release
|
||||
milestone when the change affects that product boundary. Choose the smallest
|
||||
runtime check that proves the behavior:
|
||||
|
||||
- **Server or API changes:** Exercise the changed endpoint and its meaningful auth or failure path. Add a health check only when startup or routing changed.
|
||||
- **Web changes:** Open the changed route and verify its primary interaction, keyboard flow, and failure state.
|
||||
|
|
@ -279,10 +285,10 @@ docs: update README with deployment instructions
|
|||
3. **Open a PR** against `main`.
|
||||
4. **Fill out the PR template** — describe changes, link related issues, include screenshots for UI changes.
|
||||
5. **Ensure the selected PR CI tier passes** — all checks started for the pull
|
||||
request must be green. The scope selector runs related tests for affected
|
||||
workspaces and records its base/head evidence in the job summary. Use
|
||||
`ci:full` for release candidates, critical integration/security boundaries,
|
||||
or other changes that require an explicit complete-suite gate.
|
||||
request must be green. The scope selector records affected workspaces but
|
||||
defers their tests on ordinary pull requests. Use `ci:full` for release
|
||||
candidates, critical integration/security boundaries, or other changes that
|
||||
require an explicit complete-suite gate.
|
||||
6. **Request review** — a maintainer will review and may request changes.
|
||||
7. **Address feedback** — push additional commits as needed.
|
||||
8. **Merge** — once approved, a maintainer will merge.
|
||||
|
|
@ -305,12 +311,19 @@ Follow the existing conventions in `.eslintrc.*`, `.prettierrc`, and `tsconfig.j
|
|||
pnpm test
|
||||
```
|
||||
|
||||
This is the canonical unit gate. It builds the shared package, then runs the
|
||||
server, web, CLI, and MCP suites sequentially with at most four Vitest workers
|
||||
per project. The final line reports PASS, FAIL, or NOT RUN for every workspace.
|
||||
|
||||
- **End-to-end tests** use [Playwright](https://playwright.dev/):
|
||||
|
||||
```bash
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
Playwright does not retry failures. Screenshots and traces from the first
|
||||
failure are retained in `test-results/` and uploaded by Scheduled QA.
|
||||
|
||||
- **Load smoke tests** use [k6](https://k6.io/):
|
||||
|
||||
```bash
|
||||
|
|
@ -329,32 +342,24 @@ Follow the existing conventions in `.eslintrc.*`, `.prettierrc`, and `tsconfig.j
|
|||
|
||||
### CI tiers
|
||||
|
||||
| Trigger | Stable checks | Scope |
|
||||
| ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| Documentation-only pull request or merge | Static gates; unit-test jobs record skip decisions | No workspace unit suite |
|
||||
| Ordinary code pull request | `Lint & Type Check`, `Changed Tests`, `Build`, `Security Audit` | Vitest `related` coverage for affected server, web, CLI, or MCP workspaces |
|
||||
| Ordinary code merge to `main` | Default static gates plus `Changed Tests` | Related coverage limited to affected workspaces |
|
||||
| Pull request with `ci:full`, or a CI selector/workflow control change | Default checks plus `Workspace Unit Tests` | Complete workspace, desktop readiness regressions, and exact dual-storage parity |
|
||||
| Merge whose reviewed head already passed `Workspace Unit Tests` | Static gates; both unit-test tiers record skip decisions | Reuses exact successful head evidence when that head is an ancestor of the merge commit |
|
||||
| Nightly 08:00 UTC or manual `CI` dispatch with `test_scope=full` | Static gates, `Workspace Unit Tests`, `Build`, `Security Audit` | Complete authoritative workspace suite |
|
||||
| Manual `CI` dispatch with `test_scope=focused` and optional `base_sha` | Static gates plus the selected unit-test tier | Classifies `base_sha...HEAD` (or `HEAD^...HEAD`) and stays focused unless CI controls changed |
|
||||
| Desktop/package/release-workflow pull request, relevant `main` push, or manual `Desktop Artifacts` dispatch | Unsigned macOS, Linux, and Windows artifact jobs | Cross-platform packaging |
|
||||
| Trigger | Stable checks | Scope |
|
||||
| ---------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| Documentation-only pull request or merge | Static gates; test jobs record skip decisions | No workspace tests |
|
||||
| Ordinary code pull request or merge to `main` | `Lint & Type Check`, `Build`, `Security Audit`, scope recording | No workspace tests or coverage; affected packages remain visible |
|
||||
| Pull request with `ci:full` | Default checks plus every milestone test and artifact gate | Complete unit, coverage, desktop, Docker, and applicable integration gates |
|
||||
| Nightly 08:00 UTC or manual `CI` dispatch with `test_scope=full` | Static gates plus complete workspace and coverage gates | Authoritative recurring or operator-selected milestone |
|
||||
| Manual `CI` dispatch with `test_scope=focused` and optional `base_sha` | Static gates plus `Changed Tests` | Explicit diagnostic slice for affected workspaces; no coverage ratchet |
|
||||
| Manual `Desktop Artifacts` or `Docker Image Contract` dispatch | Selected artifact or container contract | Explicit operator milestone outside a pull request |
|
||||
|
||||
`Select Test Scope` is the decision record for each run. Its summary names the
|
||||
event, exact base/head range, changed-path count, selected tier, affected
|
||||
workspaces, and why `Changed Tests` or `Workspace Unit Tests` ran or skipped.
|
||||
The selector fails safe to the complete suite for unknown non-documentation
|
||||
paths and for changes to:
|
||||
|
||||
- GitHub Actions workflows and the selector itself
|
||||
- full-suite evidence validation
|
||||
|
||||
Shared contracts, package manifests, lockfiles, storage implementations,
|
||||
desktop source, and known-workspace deletions select focused affected
|
||||
workspaces. Build and typecheck remain whole-repository gates on every ordinary
|
||||
code pull request. The full workspace suite runs at scheduled, explicit
|
||||
`ci:full`, critical integration/security, and release milestones instead of
|
||||
being repeated for every source slice.
|
||||
desktop source, and known-workspace deletions are recorded as affected
|
||||
workspaces without launching tests. Build and typecheck remain
|
||||
whole-repository gates on every ordinary code pull request. The full workspace
|
||||
suite and release-grade artifact gates run at scheduled, explicit `ci:full`,
|
||||
critical integration/security, and release milestones.
|
||||
|
||||
Run the selector contract locally with:
|
||||
|
||||
|
|
@ -365,11 +370,11 @@ pnpm test:ci-scope
|
|||
Release validation remains the final authority: clean-clone build, full unit
|
||||
and integration suites, applicable E2E, and signed artifact verification.
|
||||
|
||||
The operational target for the default pull-request tier is under 15 minutes,
|
||||
with no desktop packaging. This is a target rather than an SLA; dependency
|
||||
installation and hosted-runner availability still vary. Behavior changes
|
||||
should include coverage reachable from the changed source so Vitest's related
|
||||
test selection can execute it.
|
||||
The operational target for the default pull-request tier is under 10 minutes,
|
||||
with no workspace tests, coverage, or desktop/container packaging. This is a
|
||||
target rather than an SLA; dependency installation and hosted-runner
|
||||
availability still vary. Behavior changes should include coverage that the
|
||||
next declared milestone can exercise.
|
||||
|
||||
Optional `Desktop Artifacts`, packaging previews, and release workflows are not
|
||||
merge blockers outside their path boundary. If one starts without providing
|
||||
|
|
|
|||
79
Dockerfile
79
Dockerfile
|
|
@ -5,16 +5,17 @@
|
|||
# 1. deps — Install all workspace dependencies (shared cache layer)
|
||||
# 2. build-shared — Build the shared package
|
||||
# 3. build-web — Build React frontend with Vite
|
||||
# 4. build-server — Compile Express server TypeScript
|
||||
# 5. production — Minimal runtime image
|
||||
# 4. build-server — Compile the Express server TypeScript
|
||||
# 5. production-deps — Install the server-only runtime closure
|
||||
# 6. production — Minimal runtime image
|
||||
#
|
||||
# Target image size: < 200MB
|
||||
# Target image size: < 200,000,000 bytes on arm64; < 600,000,000 bytes on amd64
|
||||
# =============================================================================
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 1: Install dependencies (shared across build stages)
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM node:22-alpine AS deps
|
||||
FROM node:22-alpine3.24 AS deps
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@11.1.1 --activate
|
||||
|
||||
|
|
@ -63,45 +64,54 @@ COPY server/ ./server/
|
|||
RUN pnpm --filter @veritas-kanban/server build
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 5: Production runtime
|
||||
# Stage 5: Install the server-only production dependency closure
|
||||
# ---------------------------------------------------------------------------
|
||||
FROM node:22-alpine AS production
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@11.1.1 --activate
|
||||
|
||||
# Security: run as non-root
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S veritas -u 1001 -G nodejs
|
||||
FROM node:22-alpine3.24 AS production-deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy workspace config for pnpm (include real web/package.json for lockfile integrity)
|
||||
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
|
||||
COPY shared/package.json ./shared/
|
||||
COPY server/package.json ./server/
|
||||
COPY web/package.json ./web/
|
||||
COPY cli/package.json ./cli/
|
||||
COPY mcp/package.json ./mcp/
|
||||
COPY scripts/ ./scripts/
|
||||
RUN corepack enable && \
|
||||
corepack prepare pnpm@11.1.1 --activate && \
|
||||
HUSKY=0 pnpm install --frozen-lockfile --prod --filter @veritas-kanban/server... && \
|
||||
rm -rf /root/.cache/node/corepack /root/.local/share/pnpm/store /root/.local/share/pnpm/.tools
|
||||
|
||||
# Install production-only dependencies
|
||||
# --ignore-scripts: skip husky prepare hook (not needed in container)
|
||||
# Note: web deps get installed to satisfy the lockfile, but we remove them
|
||||
# since the frontend is pre-built as static assets
|
||||
RUN pnpm install --frozen-lockfile --prod --ignore-scripts && \
|
||||
rm -rf web/node_modules && \
|
||||
pnpm store prune
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 6: Production runtime
|
||||
# ---------------------------------------------------------------------------
|
||||
# The matching Alpine base keeps Node's musl ABI while excluding npm,
|
||||
# Corepack, headers, and package-manager tooling from the runtime image.
|
||||
FROM alpine:3.24 AS production
|
||||
|
||||
# Copy built artifacts
|
||||
COPY --from=build-shared /app/shared/dist ./shared/dist
|
||||
COPY --from=build-server /app/server/dist ./server/dist
|
||||
COPY --from=build-web /app/web/dist ./web/dist
|
||||
RUN apk add --no-cache ca-certificates libstdc++ && \
|
||||
addgroup -g 1001 -S nodejs && \
|
||||
adduser -S veritas -u 1001 -G nodejs
|
||||
|
||||
# Create data directories for persistent storage and runtime config
|
||||
# Note: services resolve .veritas-kanban from both cwd/.. and cwd directly,
|
||||
# so we create it at /app/ level AND ensure server/ is writable for services
|
||||
# that use process.cwd()/.veritas-kanban when WORKDIR is /app/server
|
||||
RUN mkdir -p /app/data /app/.veritas-kanban /app/tasks && \
|
||||
chown -R veritas:nodejs /app/data /app/.veritas-kanban /app/tasks /app/server
|
||||
COPY --from=production-deps /usr/local/bin/node /usr/local/bin/node
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only the resolved server runtime closure. The platform-specific Codex
|
||||
# binary remains available, while npm, pnpm, workspace manifests, and build
|
||||
# tooling never enter the production stage.
|
||||
COPY --from=production-deps --chown=veritas:nodejs /app/node_modules ./node_modules
|
||||
COPY --from=production-deps --chown=veritas:nodejs /app/server/node_modules ./server/node_modules
|
||||
COPY --from=production-deps --chown=veritas:nodejs /app/shared/package.json ./shared/package.json
|
||||
COPY --from=production-deps --chown=veritas:nodejs /app/server/package.json ./server/package.json
|
||||
|
||||
# Copy only built runtime artifacts. CLI, MCP, frontend dependencies, source,
|
||||
# and build tooling never enter the production stage.
|
||||
COPY --from=build-shared --chown=veritas:nodejs /app/shared/dist ./shared/dist
|
||||
COPY --from=build-server --chown=veritas:nodejs /app/server/dist ./server/dist
|
||||
COPY --from=build-web --chown=veritas:nodejs /app/web/dist ./web/dist
|
||||
|
||||
# Create the single volume-backed storage root. Runtime state is stored at
|
||||
# /app/data/.veritas-kanban and task data at /app/data/tasks.
|
||||
RUN mkdir -p /app/data && \
|
||||
chown -R veritas:nodejs /app/data /app/server
|
||||
|
||||
# Switch to non-root user
|
||||
USER veritas
|
||||
|
|
@ -117,8 +127,7 @@ EXPOSE 3001
|
|||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
|
||||
|
||||
# Set working directory to server/ so path.resolve(cwd, '..') resolves to /app
|
||||
# (Services use process.cwd()/.. to find .veritas-kanban and tasks directories)
|
||||
# The runtime path contract is independent of cwd when DATA_DIR is set.
|
||||
WORKDIR /app/server
|
||||
|
||||
# Start server
|
||||
|
|
|
|||
96
README.md
96
README.md
|
|
@ -10,11 +10,11 @@ Start with a visual Kanban board. Add CLI, MCP, OpenClaw, Squad Chat webhooks, w
|
|||
|
||||
[](https://github.com/BradGroux/veritas-kanban/actions/workflows/ci.yml)
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](CONTRIBUTING.md)
|
||||
|
||||

|
||||

|
||||
|
||||
> 🎬 [Watch the full demo video](https://bradgroux.github.io/veritas-kanban/demo/)
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ Want to take the easy way out? Ask your agent:
|
|||
Clone and set up veritas-kanban locally using the board-only setup path first. Install dependencies with pnpm, copy server/.env.example to server/.env, and start the dev server. Verify the UI at localhost:3000 and the API health endpoint at localhost:3001/api/health. Do not configure OpenClaw, MCP, Squad Chat webhooks, workflows, or notifications unless I explicitly ask for that layer.
|
||||
```
|
||||
|
||||
Want to do it yourself? Get up and running in under 5 minutes:
|
||||
Want to do it yourself? Choose the packaged Mac app or a local source checkout:
|
||||
|
||||
For the packaged Mac desktop app:
|
||||
|
||||
|
|
@ -103,14 +103,13 @@ When the board is working, use [Setup Paths](docs/SETUP-PATHS.md) to choose the
|
|||
- [Veritas Cutover Operating Guide](docs/VERITAS-CUTOVER.md) — authority model, HermesAgent roster, QA evidence gate, and GitHub-backed task templates.
|
||||
- [Codex Integration SOP](docs/SOP-codex-integration.md) & [Codex Workflow Examples](docs/EXAMPLES-codex-workflows.md) — operational playbooks for using Codex as a first-class Veritas agent.
|
||||
- [API Reference](docs/API-REFERENCE.md) — Auth, endpoints, request/response examples, WebSocket, common workflows.
|
||||
- [v5 Identity and RBAC Model](docs/IDENTITY-RBAC.md) — users, workspaces, memberships, roles, agent tokens, permission matrix, migration, and UX flows.
|
||||
- [v5 Mantine Migration Plan](docs/UI-MANTINE-MIGRATION.md) — component inventory, migration order, retained custom surfaces, rollback strategy, and cleanup gates.
|
||||
- [Identity and RBAC Model](docs/IDENTITY-RBAC.md) — users, workspaces, memberships, roles, agent tokens, permission matrix, migration, and UX flows.
|
||||
- [v6 GA Checklist](docs/V6-GA-CHECKLIST.md) — release gates for harness certification, migration, runtime, desktop, and distribution evidence.
|
||||
- [v6 Visual Tour](docs/V6-VISUAL-TOUR.md) — release-safe views of provider support, Buzz setup, approvals, and run evidence.
|
||||
- [v6 Upgrade, Install, Remote, And Admin Guide](docs/V6-UPGRADE-INSTALL-ADMIN-GUIDE.md) — fresh install, v5-to-v6 upgrade, harness setup, desktop, backup, and diagnostics paths.
|
||||
- [v6 Compatibility And Release Policy](docs/V6-COMPATIBILITY-AND-RELEASE-POLICY.md) — provider support tiers, tested builds, platform combinations, update channels, and rollback limits.
|
||||
- [v6 Release Notes](docs/V6-RELEASE-NOTES.md) — user-facing highlights, stabilization fixes, install/upgrade steps, behavior changes, and known limits.
|
||||
- [v5 Desktop Architecture ADR](docs/architecture/ADR-0001-v5-desktop-architecture.md) — shell decision, native/server boundaries, connection modes, lifecycle, packaging, and security model.
|
||||
- [Desktop Architecture ADR](docs/architecture/ADR-0001-v5-desktop-architecture.md) — shell decision, native/server boundaries, connection modes, lifecycle, packaging, and security model.
|
||||
- [Post-GA Desktop Agent Workbench Spec](docs/DESKTOP-AGENT-WORKBENCH.md) — desktop workbench UX, run controls, approvals, evidence, native affordances, and safety coverage.
|
||||
- [Post-GA Native Mobile Offline ADR](docs/architecture/ADR-0003-post-ga-native-mobile-offline.md) — native mobile authority model, offline queue semantics, conflict handling, and security review.
|
||||
- [Post-GA Cloud Sync And Hosted SaaS ADR](docs/architecture/ADR-0004-post-ga-cloud-sync-hosted-saas.md) — optional hosted model, tenant isolation, lifecycle, support, cost, and migration boundaries.
|
||||
|
|
@ -155,7 +154,7 @@ When the board is working, use [Setup Paths](docs/SETUP-PATHS.md) to choose the
|
|||
|
||||
8. **Isolate environments.** Run agents in containers, VMs, or sandboxed environments when possible. Keep agent workspaces separate from sensitive data, use deny-by-default network presets for untrusted work, and broker credentials instead of exposing broad environment variables.
|
||||
|
||||
**The bottom line:** Agentic AI is transformational, but it amplifies both your capabilities and your mistakes. Plan accordingly, start small, and add autonomy gradually as you build confidence in your guardrails.
|
||||
**The bottom line:** Agents amplify both useful work and mistakes. Start locally, keep permissions narrow, and add autonomy only after the smaller setup is understood and verified.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -163,7 +162,7 @@ When the board is working, use [Setup Paths](docs/SETUP-PATHS.md) to choose the
|
|||
|
||||
### 🛡️ Agent Governance
|
||||
|
||||
**Policy Engine** — Define what agents can and can't do. Configurable tool/action policies with `allow`, `deny`, and `require-approval` guard rules. Every policy decision is logged. **Sandbox Policy Presets** — Assign reusable filesystem, network, environment, and credential rules to agents, workflow agents, or one-off runs; unsupported required controls fail closed before launch with redacted audit traces. **Decision Audit Trail** — Log agent decisions with confidence scores, supporting evidence, and stated assumptions. Record outcomes afterward to see whether assumptions held. **Behavioral Drift Detection** — Set metric baselines and thresholds; get alerted when an agent's behavior deviates. **User Feedback Loop** — Collect feedback on agent outputs with sentiment tagging and category analytics. **Output Evaluation** — Score agent outputs against weighted criteria profiles (regex, keyword, numeric range, custom expressions).
|
||||
**Policy Engine** — Define what agents can and can't do. Configurable tool/action policies with `allow`, `deny`, and `require-approval` guard rules. Every policy decision is logged. **Sandbox Policy Presets** — Assign reusable filesystem, network, environment, and credential rules to agents, workflow agents, or one-off runs; unsupported required controls fail closed before launch with redacted audit traces. **Decision Audit Trail** — Log agent decisions with confidence scores, supporting evidence, and stated assumptions. Record outcomes afterward to see whether assumptions held. **Behavioral Drift Detection** — Set metric baselines and thresholds; get alerted when an agent's behavior deviates. **User Feedback Loop** — Collect feedback on agent outputs with sentiment tagging and category analytics. **Output Evaluation** — Score agent outputs against weighted bounded criteria profiles (regex, keyword, numeric range, occurrence ratio).
|
||||
|
||||
### 🤖 Agent Orchestration
|
||||
|
||||
|
|
@ -179,9 +178,9 @@ keep the board, header, close control, and keyboard recovery paths reachable.
|
|||
|
||||

|
||||
|
||||
### 🧭 Veritas Cutover + Hermes Support
|
||||
### 🧭 Provider And Cutover Operations
|
||||
|
||||
Veritas now documents the GitHub-backed operating model for Codex and HermesAgent work. The new cutover guide names Veritas as the source of truth, routes HermesAgent/Hermes Gateway as the control plane for agent execution, keeps Mission Control focused on display/control, and makes GitHub Issues/PRs/reviews/CI the implementation record. It also adds the active Hermes roster, required QA evidence gates, and copy/paste task templates for product specs, research/revenue intake, and approval-gated client workflows.
|
||||
The cutover guide documents a GitHub-backed operating model for Codex and HermesAgent work. Veritas remains the source of truth, HermesAgent/Hermes Gateway can provide the execution control plane, and GitHub Issues, pull requests, reviews, and CI remain the durable implementation record. Copy/paste task templates cover product specs, research intake, and approval-gated client workflows.
|
||||
|
||||
### 🧠 OpenAI Codex Integration
|
||||
|
||||
|
|
@ -211,9 +210,13 @@ Not just cards on a board. Tasks have dependency graphs with cycle detection, cr
|
|||
|
||||
Isolated worktrees per task — no branch switching, no conflicts. Built-in code review with unified diff viewer and inline comments. Approval workflows (approve, request changes, reject). Visual merge conflict resolution. Create GitHub PRs directly from the task detail panel. Bidirectional GitHub Issues sync with label mapping.
|
||||
|
||||
### 📁 Zero Infrastructure
|
||||
### 📁 Local-First Storage
|
||||
|
||||
Tasks are markdown files. Settings are JSON. Workflows are YAML. No database, no Redis, and no Docker required for local use. Clone, `pnpm install`, `pnpm dev` — done. Everything is `grep`-friendly, version-controllable, and human-readable. Back up your entire board with `git push`.
|
||||
File storage remains the zero-infrastructure default: tasks are Markdown,
|
||||
settings are JSON, and workflows are YAML. SQLite is available for governed
|
||||
multi-user and higher-integrity deployments; Redis and Docker are not required
|
||||
for local use. Clone, `pnpm install`, and `pnpm dev` to start. Back up the
|
||||
complete configured storage root, not only the Git-tracked board files.
|
||||
|
||||
### 🔌 Optional Integration Surfaces
|
||||
|
||||
|
|
@ -324,7 +327,7 @@ Tasks are markdown files. Settings are JSON. Workflows are YAML. No database, no
|
|||
- **Activity page** — Status history with clickable task navigation, color-coded badges, and daily summary
|
||||
- **Daily standup summary** — Generate standup reports via API or CLI (`vk summary standup`)
|
||||
- **Task Templates** — Create reusable templates with defaults, subtasks, and multi-task blueprints
|
||||
- **Documentation freshness** — Steward workflow with freshness headers and automated staleness detection
|
||||
- **Documentation freshness** — Registry-backed review dates, thresholds, scores, and staleness alerts
|
||||
- **Cost prediction** — Multi-factor cost estimation for tasks
|
||||
|
||||
#### Dashboard
|
||||
|
|
@ -373,14 +376,14 @@ Tasks are markdown files. Settings are JSON. Workflows are YAML. No database, no
|
|||
|
||||
| Layer | Technology | Version |
|
||||
| ------------------- | ------------------------------------- | ------------------------------------------- |
|
||||
| **Frontend** | React, Vite, Tailwind CSS, Mantine UI | React 19, Vite 8, Tailwind 4.3, Mantine 9.3 |
|
||||
| **Frontend** | React, Vite, Tailwind CSS, Mantine UI | React 19, Vite 8, Tailwind 4.3, Mantine 9.5 |
|
||||
| **Backend** | Express, WebSocket | Express 5.2 |
|
||||
| **Language** | TypeScript (strict mode) | 6.0 |
|
||||
| **Storage** | Markdown files with YAML frontmatter | yaml + local frontmatter helper |
|
||||
| **Git** | simple-git, worktree management | — |
|
||||
| **Testing** | Playwright (E2E), Vitest (unit) | Playwright 1.61, Vitest 4.1 |
|
||||
| **Runtime** | Node.js | 22+ |
|
||||
| **Package Manager** | pnpm | 11.1.1+ |
|
||||
| **Testing** | Playwright (E2E), Vitest (unit) | Playwright 1.62, Vitest 4.1 |
|
||||
| **Runtime** | Node.js | 22.22.1+ |
|
||||
| **Package Manager** | pnpm | 11.1.1 (pinned) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -483,7 +486,7 @@ veritas-kanban/ ← pnpm monorepo
|
|||
└── agent-requests/
|
||||
```
|
||||
|
||||
**Data flow:** Web ↔ REST API / WebSocket ↔ Server ↔ Markdown/YAML files on disk
|
||||
**Data flow:** Web ↔ REST API / WebSocket ↔ Server ↔ configured file or SQLite storage
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -512,7 +515,7 @@ curl -H "X-API-Version: v1" http://localhost:3001/api/tasks
|
|||
|
||||
> 📖 **Comprehensive CLI guide:** [docs/CLI-GUIDE.md](docs/CLI-GUIDE.md) — installation, every command, scripting examples, and tips.
|
||||
|
||||
Manage your entire task lifecycle with two commands.
|
||||
Handle the common start-and-complete task lifecycle with two commands.
|
||||
|
||||
```bash
|
||||
# Install globally
|
||||
|
|
@ -795,7 +798,7 @@ Verify discovery with `openclaw mcp list`. See [Troubleshooting](docs/TROUBLESHO
|
|||
**Troubleshooting MCP connection issues:**
|
||||
|
||||
- **Always restart the MCP client after MCP config changes** — MCP servers are discovered at startup
|
||||
- **Verify tools are available:** Run `openclaw mcp list` to confirm 41 Veritas Kanban tools appear
|
||||
- **Verify tools are available:** Run `openclaw mcp list` to confirm 42 Veritas Kanban tools appear
|
||||
- **When reporting issues, provide:**
|
||||
- OpenClaw version (`openclaw --version`)
|
||||
- VK version and health (`curl http://localhost:3001/api/health`)
|
||||
|
|
@ -837,7 +840,7 @@ pnpm build # Production build
|
|||
pnpm typecheck # TypeScript strict check
|
||||
pnpm lint # ESLint
|
||||
pnpm lint:budget # ESLint with current warning budget
|
||||
pnpm test # Unit tests (Vitest)
|
||||
pnpm test # Canonical unit gate (server, web, CLI, MCP)
|
||||
pnpm test:e2e # E2E tests (Playwright)
|
||||
pnpm test:load:smoke # k6 API smoke test
|
||||
pnpm validate:release # Release readiness checks
|
||||
|
|
@ -847,27 +850,27 @@ pnpm validate:release # Release readiness checks
|
|||
|
||||
## 📚 Documentation
|
||||
|
||||
| Document | Description |
|
||||
| ---------------------------------------------- | ------------------------------------------------ |
|
||||
| [Features](docs/FEATURES.md) | Complete feature reference |
|
||||
| [v6 Visual Tour](docs/V6-VISUAL-TOUR.md) | Provider, Buzz, approval, and run evidence views |
|
||||
| [API Reference](docs/API-REFERENCE.md) | Auth, endpoints, WebSocket docs |
|
||||
| [CLI Guide](docs/CLI-GUIDE.md) | Comprehensive CLI usage guide |
|
||||
| [Self-Hosting Guide](docs/guides/SELF_HOST.md) | Production deployment, reverse proxy, Docker |
|
||||
| [Deployment](docs/DEPLOYMENT.md) | Docker, bare metal, env config |
|
||||
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common issues & solutions |
|
||||
| [Contributing](CONTRIBUTING.md) | How to contribute, PR guidelines |
|
||||
| [Security Policy](SECURITY.md) | Vulnerability reporting |
|
||||
| [Code of Conduct](CODE_OF_CONDUCT.md) | Community guidelines |
|
||||
| [Changelog](CHANGELOG.md) | Release history |
|
||||
| [Sprint Docs](docs/) | Sprint planning & audit reports |
|
||||
| Document | Description |
|
||||
| ---------------------------------------------- | --------------------------------------------------- |
|
||||
| [Features](docs/FEATURES.md) | Complete feature reference |
|
||||
| [v6 Visual Tour](docs/V6-VISUAL-TOUR.md) | Provider, Buzz, approval, and run evidence views |
|
||||
| [API Reference](docs/API-REFERENCE.md) | Auth, endpoints, WebSocket docs |
|
||||
| [CLI Guide](docs/CLI-GUIDE.md) | Comprehensive CLI usage guide |
|
||||
| [Self-Hosting Guide](docs/guides/SELF_HOST.md) | Production deployment, reverse proxy, Docker |
|
||||
| [Deployment](docs/DEPLOYMENT.md) | Docker, bare metal, env config |
|
||||
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common issues and solutions |
|
||||
| [Contributing](CONTRIBUTING.md) | How to contribute and pull request guidelines |
|
||||
| [Security Policy](SECURITY.md) | Vulnerability reporting |
|
||||
| [Code of Conduct](CODE_OF_CONDUCT.md) | Community guidelines |
|
||||
| [Changelog](CHANGELOG.md) | Release history |
|
||||
| [Documentation Index](docs/) | Operator, developer, architecture, and release docs |
|
||||
|
||||
---
|
||||
|
||||
## 📸 v5 Visuals
|
||||
## 📸 Visuals
|
||||
|
||||
<details>
|
||||
<summary><strong>Click to expand v5 screenshots and GIFs</strong></summary>
|
||||
<summary><strong>Click to expand screenshots and GIFs</strong></summary>
|
||||
|
||||
These captures use release-safe dummy content against the current app surfaces. See the [v6 Visual Tour](docs/V6-VISUAL-TOUR.md) for the current release views and retained v5 shell captures.
|
||||
|
||||
|
|
@ -901,19 +904,22 @@ These captures use release-safe dummy content against the current app surfaces.
|
|||
|
||||
## 🗺️ Roadmap
|
||||
|
||||
Current planning lives in GitHub, not in a stale README checklist:
|
||||
Current work and priorities live in GitHub, not in a version-specific README checklist:
|
||||
|
||||
- [Open issues](https://github.com/BradGroux/veritas-kanban/issues)
|
||||
- [v5.0 roadmap issues](https://github.com/BradGroux/veritas-kanban/issues?q=is%3Aissue%20state%3Aopen%20label%3Arelease%3Av5.0)
|
||||
- [v5.0 SQLite schema and migration strategy](docs/SQLITE-SCHEMA.md)
|
||||
- [v5.0 SQLite migration recovery drill](docs/MIGRATION-RECOVERY.md)
|
||||
- [v5.0 desktop architecture decision](docs/architecture/ADR-0001-v5-desktop-architecture.md)
|
||||
- [post-GA desktop agent workbench spec](docs/DESKTOP-AGENT-WORKBENCH.md)
|
||||
- [Release history](CHANGELOG.md)
|
||||
- [GitHub releases](https://github.com/BradGroux/veritas-kanban/releases)
|
||||
|
||||
Longer-lived product and architecture direction is recorded separately:
|
||||
|
||||
- [v6 agent runtime control plane](docs/architecture/V6-AGENT-RUNTIME-CONTROL-PLANE.md)
|
||||
- [phase capability profiles](docs/architecture/PHASE-CAPABILITY-PROFILES.md)
|
||||
- [tool control plane v1](docs/architecture/TOOL-CONTROL-PLANE-V1.md)
|
||||
- [post-GA desktop agent workbench](docs/DESKTOP-AGENT-WORKBENCH.md)
|
||||
- [post-GA native mobile offline decision](docs/architecture/ADR-0003-post-ga-native-mobile-offline.md)
|
||||
- [post-GA cloud sync and hosted SaaS decision](docs/architecture/ADR-0004-post-ga-cloud-sync-hosted-saas.md)
|
||||
- [Release history](CHANGELOG.md)
|
||||
|
||||
Use issues for current work and the changelog for shipped work.
|
||||
Use issues for current work, architecture records for durable direction, and the changelog and releases for shipped work.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
44
SECURITY.md
44
SECURITY.md
|
|
@ -53,6 +53,50 @@ GitHub secret scanning and push protection should remain enabled for the
|
|||
repository. The tracked-file guard complements those services because generic
|
||||
password and recovery-key hashes may not match provider-specific signatures.
|
||||
|
||||
## CI Supply Chain Integrity
|
||||
|
||||
Every external GitHub Action and reusable workflow reference must use a full
|
||||
40-character commit SHA followed by a readable release comment. Local actions
|
||||
under `./.github/actions/` are reviewed with the repository and do not need a
|
||||
remote revision. Docker actions must use a complete SHA-256 image digest.
|
||||
|
||||
The same policy is enforced locally and in CI:
|
||||
|
||||
```bash
|
||||
pnpm check:actions-pinned
|
||||
```
|
||||
|
||||
Dependabot retains the `github-actions` ecosystem entry so reviewed updates can
|
||||
advance both the immutable commit and its release comment.
|
||||
|
||||
## Continuous Security Gates
|
||||
|
||||
The `Security Gates` workflow runs CodeQL and gitleaks for pull requests, main
|
||||
branch updates, and a weekly schedule. CodeQL uses the extended JavaScript and
|
||||
TypeScript security query suite. Repository merge protection blocks CodeQL
|
||||
errors and high-or-critical security alerts. Gitleaks scans the current tree,
|
||||
accepts only the exact reviewed fingerprints in `.gitleaksignore`, and proves
|
||||
that a newly introduced synthetic secret is still rejected.
|
||||
|
||||
Brad Groux owns Dependabot and GitHub security alert triage. New dependency,
|
||||
code-scanning, or secret-scanning alerts must be reviewed privately within two
|
||||
working days. Confirm exploitability and affected releases before opening a
|
||||
public issue. Track confirmed vulnerabilities in a private GitHub security
|
||||
advisory, prioritize critical and high findings for the next safe patch, and
|
||||
record false positives at the narrowest available fingerprint or path. Do not
|
||||
disable a detector class to clear a gate.
|
||||
|
||||
Dependabot vulnerability alerts and security updates, GitHub secret scanning,
|
||||
and push protection must remain enabled. Security-update pull requests use the
|
||||
existing `BradGroux` reviewer assignment in `.github/dependabot.yml`.
|
||||
|
||||
Run the repository controls locally with:
|
||||
|
||||
```bash
|
||||
pnpm check:security-gates
|
||||
pnpm check:gitleaks
|
||||
```
|
||||
|
||||
## Scope
|
||||
|
||||
This policy applies to:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@veritas-kanban/cli",
|
||||
"version": "6.1.1",
|
||||
"version": "6.1.2",
|
||||
"description": "CLI for Veritas Kanban task management",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsx src/index.ts",
|
||||
"test": "vitest run --maxWorkers=4",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -5,5 +5,21 @@ export default defineConfig({
|
|||
include: ['src/**/*.test.ts'],
|
||||
exclude: ['**/node_modules/**', '**/dist/**'],
|
||||
globals: true,
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: [
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.d.ts',
|
||||
'src/__tests__/**',
|
||||
'src/**/__fixtures__/**',
|
||||
'src/**/fixtures/**',
|
||||
'src/**/generated/**',
|
||||
'src/**/*.generated.*',
|
||||
'src/**/types.ts',
|
||||
'src/types/**/*.ts',
|
||||
],
|
||||
all: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@veritas-kanban/desktop",
|
||||
"version": "6.1.1",
|
||||
"version": "6.1.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/BradGroux/veritas-kanban",
|
||||
"description": "Veritas Kanban native desktop shell",
|
||||
|
|
@ -34,12 +34,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.2.0",
|
||||
"electron": "^43.4.0",
|
||||
"electron": "^43.4.1",
|
||||
"electron-builder": "^26.15.3",
|
||||
"electron-vite": "^5.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.2.1",
|
||||
"vitest": "^4.1.10"
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"build": {
|
||||
"appId": "io.digitalmeld.veritas-kanban",
|
||||
|
|
|
|||
|
|
@ -5,5 +5,21 @@ export default defineConfig({
|
|||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
exclude: ['dist/**', 'out/**', 'node_modules/**'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: [
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.d.ts',
|
||||
'src/**/__tests__/**',
|
||||
'src/**/__fixtures__/**',
|
||||
'src/**/fixtures/**',
|
||||
'src/**/generated/**',
|
||||
'src/**/*.generated.*',
|
||||
'src/**/types.ts',
|
||||
'src/types/**/*.ts',
|
||||
],
|
||||
all: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ services:
|
|||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: veritas-kanban-demo
|
||||
# IMPORTANT: Must match Dockerfile WORKDIR (/app/server) for correct path resolution
|
||||
# Kept aligned with the image entrypoint; persistent paths resolve from DATA_DIR.
|
||||
working_dir: /app/server
|
||||
ports:
|
||||
# Demo instance port (do NOT use production 3001)
|
||||
|
|
|
|||
|
|
@ -681,6 +681,14 @@ Callback and remote-session terminal sources are accepted only for OpenClaw.
|
|||
CLI process and SDK stream providers reject callback transport even when an
|
||||
attempt ID and manifest digest are known.
|
||||
|
||||
Terminal task mutation crosses one `AttemptLifecycleCoordinator` seam. The
|
||||
coordinator validates the persisted runtime, envelope, and optional launch
|
||||
manifest bindings; enforces active-attempt ownership; retries bounded task
|
||||
revision conflicts; updates current and historical attempt state together;
|
||||
and treats only the exact persisted idempotency key as a safe duplicate.
|
||||
Provider adapters and restart recovery prepare evidence but cannot implement a
|
||||
parallel terminal persistence path.
|
||||
|
||||
Provider summaries, evidence, artifacts, and verification claims are bounded,
|
||||
redacted, and stored as unverified provider evidence. Veritas independently
|
||||
captures Git HEAD, post-launch files and commits, task verification state,
|
||||
|
|
@ -1065,6 +1073,22 @@ execution-tree identity to the durable attempt. Missing or inconsistent
|
|||
evidence fails before the adapter is called, so an adapter cannot widen
|
||||
capacity or substitute an external hidden queue.
|
||||
|
||||
### Provider adapter lifecycle ownership
|
||||
|
||||
`server/src/services/agent-provider-adapter-registry.ts` is the executable
|
||||
provider-selection authority. Its `resolve(provider, surface)` interface owns
|
||||
the exact adapter identity, task-envelope renderer, runtime probe, run-event
|
||||
mapper, start dispatch, and stop behavior for every executable provider.
|
||||
Unknown or non-executable providers fail before this seam; there is no implicit
|
||||
OpenClaw fallback.
|
||||
|
||||
`ClawdbotAgentService` remains the shared run orchestrator. It supplies
|
||||
admission, supervisor, sandbox, budget, journal, and completion effects to the
|
||||
registry host without duplicating provider selection. Full attempt mutations
|
||||
cross `AttemptLifecycleCoordinator`, which verifies active-attempt ownership,
|
||||
optimistic revisions, history maintenance, and terminal completion binding.
|
||||
Provider adapters never write attempt state directly.
|
||||
|
||||
Active leases renew while the verified run is live; completion, interruption,
|
||||
cancellation, or launch failure releases the reservation idempotently.
|
||||
Workflow retry and fallback attempts release the prior step reservation before
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ The Agent Registry is a service discovery and liveness tracking system for AI ag
|
|||
| **Persistence** | File-backed JSON survives server restarts |
|
||||
| **Dashboard** | Live agent cards in the board sidebar |
|
||||
|
||||
**Storage:** `.veritas-kanban/agent-registry.json`
|
||||
**Storage:** `<storage-root>/.veritas-kanban/agent-registry.json`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -406,13 +406,14 @@ The panel reads from the registry API and updates every 30 seconds (plus WebSock
|
|||
| ------------------------- | ------------------ | -------------------------------------------- |
|
||||
| `HEARTBEAT_TIMEOUT_MS` | 300,000 (5 min) | Time before marking agent offline |
|
||||
| `STALE_CHECK_INTERVAL_MS` | 60,000 (1 min) | How often the server checks for stale agents |
|
||||
| `VERITAS_DATA_DIR` | `.veritas-kanban/` | Directory for registry JSON file |
|
||||
| `VERITAS_DATA_DIR` | Project root | Storage root used when `DATA_DIR` is unset |
|
||||
|
||||
---
|
||||
|
||||
## File Format
|
||||
|
||||
The registry is stored as JSON at `.veritas-kanban/agent-registry.json`:
|
||||
The registry is stored as JSON at
|
||||
`<storage-root>/.veritas-kanban/agent-registry.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Veritas Kanban — API Reference
|
||||
|
||||
**Version**: 6.1.1
|
||||
**Last Updated**: 2026-07-25
|
||||
**Version**: 6.1.2
|
||||
**Last Updated**: 2026-08-24
|
||||
**Base URL**: `http://localhost:3001/api`
|
||||
**Canonical prefix**: `/api/v1` (alias: `/api`)
|
||||
|
||||
|
|
@ -5799,6 +5799,13 @@ POST /api/scoring/profiles
|
|||
|
||||
**Response:** `201` with created profile.
|
||||
|
||||
Scoring profiles accept `KeywordContains`, `NumericRange`, bounded `RegexMatch`, and declarative
|
||||
`OccurrenceRatio` scorers. Regex patterns are limited to 256 characters and execute through a
|
||||
globally bounded four-worker pool and wait queue with a 100 ms limit. Valid JavaScript regex flags
|
||||
supported by the active Node runtime remain accepted. `OccurrenceRatio` counts literal `needles`
|
||||
and normalizes them with a fixed `denominator` or numeric `denominatorPath`; it does not execute
|
||||
expression strings. Persisted legacy custom-expression profiles must be migrated before evaluation.
|
||||
|
||||
#### Get Scoring Profile
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Companion docs:
|
|||
|
||||
- [SOP: OpenAI Codex Integration](SOP-codex-integration.md)
|
||||
- [Codex Workflow Examples](EXAMPLES-codex-workflows.md)
|
||||
- [SOP: Cross-Model Code Review](SOP-cross-model-code-review.md)
|
||||
- [Optional Independent Code Review](SOP-cross-model-code-review.md)
|
||||
- [AGENTS.md Template](AGENTS-TEMPLATE.md)
|
||||
|
||||
## Product Goal
|
||||
|
|
@ -80,7 +80,17 @@ POST /api/github/codex/delegate
|
|||
|
||||
## Architecture Direction
|
||||
|
||||
v4.3 uses an explicit provider adapter contract inside the agent service. `codex` agents resolve to the local Codex CLI runner, `codex-sdk` agents resolve to the SDK session runner, `codex-cloud` uses GitHub-native delegation, and existing agents keep the OpenClaw request-file behavior.
|
||||
Executable task providers resolve through the dedicated
|
||||
`AgentProviderAdapterRegistry`. The registry owns exact provider selection,
|
||||
task-envelope rendering, runtime probing, run-event mapping, start dispatch,
|
||||
and stop semantics. `ClawdbotAgentService` supplies shared admission,
|
||||
supervision, journaling, budget, and completion effects without selecting an
|
||||
implicit fallback adapter.
|
||||
|
||||
`codex` agents resolve to the local Codex CLI runner, `codex-sdk` agents resolve
|
||||
to the SDK session runner, and `codex-cloud` uses GitHub-native delegation.
|
||||
OpenClaw task dispatch uses the gateway `sessions_spawn` path and persists the
|
||||
returned session identity on the active attempt.
|
||||
|
||||
Expected long-term provider capabilities:
|
||||
|
||||
|
|
@ -93,7 +103,7 @@ Expected long-term provider capabilities:
|
|||
- optional `review`
|
||||
- optional `cloudDelegate`
|
||||
|
||||
The provider abstraction should support:
|
||||
The provider adapter interface supports:
|
||||
|
||||
- OpenClaw compatibility through an OpenClaw provider adapter.
|
||||
- Codex CLI through a local process provider.
|
||||
|
|
|
|||
|
|
@ -67,17 +67,39 @@ Data is persisted in a Docker named volume (`kanban-data`), so it survives conta
|
|||
|
||||
### Dockerfile Overview
|
||||
|
||||
The multi-stage Dockerfile produces a minimal production image (< 200 MB):
|
||||
The multi-stage Dockerfile enforces architecture-specific production image budgets:
|
||||
|
||||
| Stage | Purpose |
|
||||
| -------------- | --------------------------------------- |
|
||||
| `deps` | Install all pnpm workspace dependencies |
|
||||
| `build-shared` | Compile the shared TypeScript package |
|
||||
| `build-web` | Build the React frontend with Vite |
|
||||
| `build-server` | Compile the Express server TypeScript |
|
||||
| `production` | Minimal Node.js 22 Alpine runtime |
|
||||
| Architecture | Maximum compressed image size | 6.1.2 implementation baseline |
|
||||
| ------------ | ----------------------------- | ----------------------------- |
|
||||
| `arm64` | 200,000,000 bytes | 195,910,880 bytes |
|
||||
| `amd64` | 600,000,000 bytes | 571,590,173 bytes |
|
||||
|
||||
The production stage runs as a non-root user (`veritas`, UID 1001) for security.
|
||||
The final release candidate is remeasured at the release milestone; these
|
||||
implementation baselines are not substituted for final artifact evidence.
|
||||
|
||||
| Stage | Purpose |
|
||||
| -------------- | ------------------------------------------------------------------------ |
|
||||
| `deps` | Install all pnpm workspace dependencies |
|
||||
| `build-shared` | Compile the shared TypeScript package |
|
||||
| `build-web` | Build the React frontend with Vite |
|
||||
| `build-server` | Compile the server and deploy its production dependency closure |
|
||||
| `production` | Copy only the server closure and built web assets into Node.js 22 Alpine |
|
||||
|
||||
The production stage does not contain npm, pnpm, the root workspace/lockfile,
|
||||
CLI dependencies, or MCP dependencies. It retains only the server and shared
|
||||
package identity manifests required for module resolution and version health.
|
||||
It runs as the non-root `veritas` user (UID 1001).
|
||||
|
||||
The `amd64` image is larger because the Linux Codex runtime bundled by `@openai/codex-sdk`
|
||||
occupies about 302 MB of its unpacked filesystem, including a roughly 245 MB executable.
|
||||
Retaining it keeps the `codex-sdk` provider functional without an operator-supplied binary.
|
||||
The budgets leave about 2% headroom on `arm64` and 5% on `amd64`, so material dependency growth
|
||||
still fails the contract instead of being normalized by one loose cross-platform ceiling.
|
||||
|
||||
CI builds the production target and runs `pnpm check:docker-image`. The contract fails when the
|
||||
image reaches its architecture budget or when the runtime smoke cannot prove non-root execution,
|
||||
SQLite startup, API authentication, static web serving, health checks, and the native `bcrypt`
|
||||
module. `VERITAS_DOCKER_MAX_BYTES` can set an explicit budget for another architecture.
|
||||
|
||||
**Path Resolution (v2.1.3):** All services use the shared `paths.ts` utility for consistent path resolution. The resolution priority is: `DATA_DIR` / `VERITAS_DATA_DIR` env var → auto-discovery of monorepo root (walks up from cwd looking for `pnpm-workspace.yaml`) → fallback to cwd. A filesystem root guard prevents silent `/` resolution, which previously caused `EACCES: permission denied` errors in Docker. The production image uses `WORKDIR /app/server` for backwards compatibility.
|
||||
|
||||
|
|
@ -232,10 +254,10 @@ If you need to debug inside a container, use `docker exec` to inspect — don't
|
|||
|
||||
### Prerequisites
|
||||
|
||||
| Requirement | Version |
|
||||
| ----------- | ------- |
|
||||
| Node.js | 22.0.0+ |
|
||||
| pnpm | 11.1.1+ |
|
||||
| Requirement | Version |
|
||||
| ----------- | --------------- |
|
||||
| Node.js | 22.22.1+ |
|
||||
| pnpm | 11.1.1 (pinned) |
|
||||
|
||||
Install pnpm if not present:
|
||||
|
||||
|
|
@ -434,18 +456,15 @@ sends API requests to `/kanban/api/...`.
|
|||
> or the equivalent changes to `vite.config.ts`, `web/src/lib/config.ts`, and
|
||||
> `web/src/lib/api/helpers.ts`.
|
||||
|
||||
**Docker volumes for sub-path:** When using Docker with sub-path deployment, ensure both
|
||||
the task data and the config directory are on persistent volumes:
|
||||
**Docker volumes for sub-path:** One volume at `DATA_DIR` persists tasks and runtime state:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- kanban-data:/app/data # Task files
|
||||
- kanban-config:/app/.veritas-kanban # Config, sprints, enforcement gates
|
||||
- kanban-data:/app/data # tasks/ plus .veritas-kanban/
|
||||
```
|
||||
|
||||
Without a config volume, settings (enforcement gates, transition hooks, sprints) are lost
|
||||
on every container rebuild because `.veritas-kanban/` lives on the overlay filesystem, not
|
||||
on the data volume.
|
||||
Do not mount a second volume at `/app/.veritas-kanban`; that is a legacy location used only
|
||||
as a read-only source during startup migration.
|
||||
|
||||
### systemd Service
|
||||
|
||||
|
|
@ -554,16 +573,16 @@ All variables are set in `server/.env` (or passed as environment variables in Do
|
|||
|
||||
### Data & Storage
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------------------- | -------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
| `VERITAS_DATA_DIR` | `.veritas-kanban` (relative to project root) | Directory for config, logs, and internal data |
|
||||
| `DATA_DIR` | `/app/data` (Docker only) | Mapped data directory inside the Docker container |
|
||||
| `VERITAS_STORAGE` | `file` | Selects `file` or `sqlite` storage |
|
||||
| `VERITAS_SQLITE_PATH` | Runtime `veritas.db` | SQLite database override; must resolve to verified durable local storage |
|
||||
| `VERITAS_SQLITE_TOPOLOGY` | — | Set explicitly to `single-host` before compatibility/override maintenance |
|
||||
| `VERITAS_SQLITE_HOST_ID` | — | Stable unique host binding for SQLite compatibility ownership policy |
|
||||
| `TELEMETRY_RETENTION_DAYS` | `30` | Days to keep telemetry event files before deletion |
|
||||
| `TELEMETRY_COMPRESS_DAYS` | `7` | Days after which NDJSON telemetry files are gzip-compressed (0 = disabled) |
|
||||
| Variable | Default | Description |
|
||||
| -------------------------- | ------------------------- | -------------------------------------------------------------------------- |
|
||||
| `VERITAS_DATA_DIR` | Project root when unset | Storage root used when `DATA_DIR` is unset |
|
||||
| `DATA_DIR` | `/app/data` (Docker only) | Preferred storage root; takes precedence over `VERITAS_DATA_DIR` |
|
||||
| `VERITAS_STORAGE` | `file` | Selects `file` or `sqlite` storage |
|
||||
| `VERITAS_SQLITE_PATH` | Runtime `veritas.db` | SQLite database override; must resolve to verified durable local storage |
|
||||
| `VERITAS_SQLITE_TOPOLOGY` | — | Set explicitly to `single-host` before compatibility/override maintenance |
|
||||
| `VERITAS_SQLITE_HOST_ID` | — | Stable unique host binding for SQLite compatibility ownership policy |
|
||||
| `TELEMETRY_RETENTION_DAYS` | `30` | Days to keep telemetry event files before deletion |
|
||||
| `TELEMETRY_COMPRESS_DAYS` | `7` | Days after which NDJSON telemetry files are gzip-compressed (0 = disabled) |
|
||||
|
||||
### Integration
|
||||
|
||||
|
|
@ -619,9 +638,25 @@ wscat -c "ws://localhost:3001/ws?api_key=<api-key>"
|
|||
| `.veritas-kanban/worktree-manifests/` | Durable worktree ownership, base, lifecycle, and override evidence |
|
||||
| `.veritas-kanban/agent-requests/` | Pending AI agent requests |
|
||||
|
||||
In Docker, the `DATA_DIR` environment variable maps to `/app/data` by default inside the container.
|
||||
In Docker, `DATA_DIR=/app/data`. Tasks live under `/app/data/tasks` and all runtime state
|
||||
lives under `/app/data/.veritas-kanban`; no persistent state is written to `/app` or
|
||||
`/app/server` outside that volume.
|
||||
|
||||
**Auth state persistence fix (v3.1.1):** Runtime config/state files (including `security.json`) now always live under `${DATA_DIR}/.veritas-kanban`. On startup, Veritas Kanban will automatically migrate any legacy runtime files it finds in container-only paths (for example, `/app/.veritas-kanban` or `/app/server/.veritas-kanban`) into the Docker volume.
|
||||
**Auth state persistence fix (v3.1.1):** Runtime config/state files (including `security.json`) now always live under `${DATA_DIR}/.veritas-kanban`. On startup, Veritas Kanban automatically migrates legacy runtime files it can see at container-only paths (for example, `/app/.veritas-kanban` or `/app/server/.veritas-kanban`) into the Docker volume. A replaced container cannot see data left in an old container layer or an unmounted legacy volume.
|
||||
|
||||
If the old runtime state is in a named volume, mount that volume read-only at its former path for one startup. For example, add the legacy mount temporarily to your Compose service:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
veritas-kanban:
|
||||
volumes:
|
||||
- kanban-data:/app/data
|
||||
- legacy-veritas-config:/app/.veritas-kanban:ro
|
||||
```
|
||||
|
||||
Start the service, verify the expected files now exist under
|
||||
`/app/data/.veritas-kanban`, then remove the legacy mount from Compose. The migration is
|
||||
copy-only: it does not delete the legacy source, and an existing destination file wins.
|
||||
|
||||
If you upgraded from an older image and already lost auth state, you can recover by copying `security.json` from a still-running/old container (if available) into the volume:
|
||||
|
||||
|
|
@ -697,7 +732,7 @@ docker compose down
|
|||
docker run --rm \
|
||||
-v kanban-data:/data \
|
||||
-v $(pwd):/backup \
|
||||
alpine sh -c "rm -rf /data/* && tar xzf /backup/veritas-backup-20260129.tar.gz -C /data"
|
||||
alpine sh -c 'set -eu; archive=/backup/veritas-backup-20260129.tar.gz; test -d /data; test "$(readlink -f /data)" = /data; test -r "$archive"; tar tzf "$archive" >/dev/null; find /data -mindepth 1 -delete; tar xzf "$archive" -C /data'
|
||||
|
||||
# Restart
|
||||
docker compose up -d
|
||||
|
|
|
|||
|
|
@ -50,9 +50,11 @@ live repository install into a production-only dependency state.
|
|||
|
||||
## GitHub Workflows
|
||||
|
||||
`Desktop Artifacts` runs on desktop/package/release-workflow pull requests,
|
||||
after server/web/shared/desktop changes merge to `main`, and on manual
|
||||
dispatch. It builds unsigned artifacts on:
|
||||
`Desktop Artifacts` runs only for a pull request carrying `ci:full` or through
|
||||
manual dispatch. Ordinary pull requests and `main` pushes do not package
|
||||
desktop applications. A release candidate keeps `ci:full` applied through its
|
||||
final synchronization so the artifacts correspond to the reviewed head. The
|
||||
workflow builds unsigned artifacts on:
|
||||
|
||||
- `macos-15`: DMG, ZIP, blockmap, and update YAML.
|
||||
- `ubuntu-24.04`: x64 AppImage, deb, rpm, blockmap, and update YAML.
|
||||
|
|
@ -187,7 +189,8 @@ policy is tracked in
|
|||
- Run `pnpm desktop:package:windows:unsigned` on Windows or the
|
||||
`Desktop Artifacts` Windows job and inspect preview artifact names. This is
|
||||
not a v6 GA release gate.
|
||||
- Run `Desktop Artifacts` and download the uploaded DMG/ZIP/update metadata.
|
||||
- Apply `ci:full` to the release-candidate pull request, then download the
|
||||
uploaded DMG/ZIP/update metadata from its `Desktop Artifacts` run.
|
||||
- Edit `docs/releases/vX.Y.Z.md`, run
|
||||
`pnpm validate:release -- --version X.Y.Z`, and publish that exact file with
|
||||
`gh release create --notes-file` or `gh release edit --notes-file`. Do not
|
||||
|
|
|
|||
|
|
@ -44,7 +44,13 @@ When completing a task that changes user-facing behavior:
|
|||
|
||||
### Freshness Indicators
|
||||
|
||||
Each doc should include a freshness header:
|
||||
The Settings → Doc Freshness registry is the authoritative freshness source.
|
||||
Each tracked record stores its path, last review date, reviewer, maximum age,
|
||||
tags, and notes. The service computes scores and alerts from those records; it
|
||||
does not scan or rewrite Markdown headers.
|
||||
|
||||
A maintained living document may also include this optional human-readable
|
||||
marker when repository reviewers find it useful:
|
||||
|
||||
```markdown
|
||||
<!-- doc-freshness: 2026-03-25 | v4.0.0 | @veritas -->
|
||||
|
|
@ -52,28 +58,32 @@ Each doc should include a freshness header:
|
|||
|
||||
Format: `date | version | last-updater`
|
||||
|
||||
When a doc is older than the current version, it may need review.
|
||||
The optional marker is not required for release notes, historical evidence,
|
||||
generated references, or every file under `docs/`. When a tracked document is
|
||||
older than its configured maximum age or its maintained version, review it and
|
||||
update the authoritative registry record.
|
||||
|
||||
### Last Sweep
|
||||
|
||||
| Date | Scope | Agent |
|
||||
| ---------- | ------------------------------------------------------------------- | ------- |
|
||||
| 2026-08-22 | v6.1.1 maintenance, dependency, release, upgrade, and evidence docs | Release |
|
||||
| 2026-07-26 | v6.1.0 roadmap, harness, governance, knowledge, and release docs | Release |
|
||||
| 2026-07-24 | v6.0.2 desktop recovery, version support, release, and evidence | Release |
|
||||
| 2026-07-24 | v6.0.1 stabilization, release, upgrade, API, MCP, and evidence | Release |
|
||||
| 2026-07-24 | v6.0.0 harness, Buzz, release, upgrade, compatibility, and evidence | Release |
|
||||
| 2026-07-12 | v5.2.2 UI-audit fixes, release gates, desktop state, and evidence | Release |
|
||||
| 2026-06-05 | v5.0.0 stable release docs, install paths, release assets, RC notes | Codex |
|
||||
| 2026-03-25 | Full v3→v4 version references, governance docs, CHANGELOG, examples | VERITAS |
|
||||
| 2026-03-21 | v4.0 release documentation | TARS |
|
||||
| Date | Scope | Agent |
|
||||
| ---------- | ------------------------------------------------------------------------------------------ | ------- |
|
||||
| 2026-08-24 | README; v6.1.2 audit, storage, provider, CI, security, release, distribution, and SOP docs | Release |
|
||||
| 2026-08-22 | v6.1.1 maintenance, dependency, release, upgrade, and evidence docs | Release |
|
||||
| 2026-07-26 | v6.1.0 roadmap, harness, governance, knowledge, and release docs | Release |
|
||||
| 2026-07-24 | v6.0.2 desktop recovery, version support, release, and evidence | Release |
|
||||
| 2026-07-24 | v6.0.1 stabilization, release, upgrade, API, MCP, and evidence | Release |
|
||||
| 2026-07-24 | v6.0.0 harness, Buzz, release, upgrade, compatibility, and evidence | Release |
|
||||
| 2026-07-12 | v5.2.2 UI-audit fixes, release gates, desktop state, and evidence | Release |
|
||||
| 2026-06-05 | v5.0.0 stable release docs, install paths, release assets, RC notes | Codex |
|
||||
| 2026-03-25 | Full v3→v4 version references, governance docs, CHANGELOG, examples | VERITAS |
|
||||
| 2026-03-21 | v4.0 release documentation | TARS |
|
||||
|
||||
## Automation Plan
|
||||
|
||||
### Phase 1: Manual (Current)
|
||||
|
||||
- Doc update checklist in PR template
|
||||
- Freshness headers in docs
|
||||
- Doc Freshness registry records, with optional source markers where useful
|
||||
- Agent instructions include "update docs" step
|
||||
|
||||
### Phase 2: Hook-Based
|
||||
|
|
@ -112,7 +122,8 @@ project template, and harness-specific files only supplement the canonical
|
|||
rules. Key rules:
|
||||
|
||||
1. **Always update docs alongside code** — no code-only PRs for user-facing changes
|
||||
2. **Use freshness headers** — every doc starts with `<!-- doc-freshness: ... -->`
|
||||
2. **Track maintained living docs** — use the Doc Freshness registry; optional
|
||||
source headers are a reviewer aid, not the system of record
|
||||
3. **JSDoc is documentation** — route handlers and services must have JSDoc
|
||||
4. **Examples must work** — if you change an API, update the examples
|
||||
5. **CHANGELOG is mandatory** — every release gets an entry
|
||||
|
|
|
|||
|
|
@ -54,11 +54,11 @@ Steal these end-to-end flows when building your own automations. Each example sh
|
|||
|
||||
## 4. Security Audit (RF-002 style)
|
||||
|
||||
**Goal:** Run cross-model audit on repo.
|
||||
**Goal:** Run a focused security audit on the repository.
|
||||
|
||||
1. Task -> `type=security`, `project=veritas-kanban`.
|
||||
2. Subtasks: scope, run Codex audit, run Claude review, compile findings, create issues.
|
||||
3. Agents spawn using research prompt template, save results to `refactoring/rf-002/*`.
|
||||
2. Subtasks: scope, inspect trust boundaries, validate findings, compile evidence, create issues.
|
||||
3. Use the security-review prompt and save durable results to the task's declared artifact path.
|
||||
4. Deliverables: Markdown report, HTML deck, GitHub issues.
|
||||
|
||||
---
|
||||
|
|
@ -88,7 +88,7 @@ For any workflow:
|
|||
2. **Prompt** stored in registry.
|
||||
3. **API/CLI** calls scripted (vk begin/done, time tracking, status updates).
|
||||
4. **Artifacts** saved to predictable paths and mirrored to Brain/engram if needed.
|
||||
5. **Cross-model review** if code/critical.
|
||||
5. **Focused review** when the task or configured governance policy requires it.
|
||||
6. **Lessons learned** field updated for systemic knowledge.
|
||||
|
||||
Use these recipes as seeds for your own automation playbooks.
|
||||
|
|
|
|||
|
|
@ -47,9 +47,10 @@ Use these recipes as starting points for v4.3 OpenAI Codex workflows in Veritas
|
|||
|
||||
---
|
||||
|
||||
## 2. Codex Review Of A Claude-authored PR
|
||||
## 2. Optional Independent Review With Codex
|
||||
|
||||
**Goal:** Use Codex as the opposite-model reviewer for a Claude-authored branch.
|
||||
**Goal:** Use Codex as an independent reviewer when a task or governance policy
|
||||
explicitly requires one. This is not a default delivery step.
|
||||
|
||||
1. Keep the original implementation task `in-progress`.
|
||||
2. Trigger a Codex review action:
|
||||
|
|
@ -152,7 +153,7 @@ steps:
|
|||
agent: reviewer
|
||||
depends_on: [implement]
|
||||
input: |
|
||||
Review Codex's implementation using docs/SOP-cross-model-code-review.md.
|
||||
Run the configured independent review using docs/SOP-cross-model-code-review.md.
|
||||
```
|
||||
|
||||
Expected behavior:
|
||||
|
|
|
|||
|
|
@ -634,11 +634,11 @@ Reusable resources mountable across projects with full CRUD API and Settings tab
|
|||
Automated staleness detection for project documentation with real-time tracking and alerting. Added in v3.2.
|
||||
|
||||
- **Freshness tracking** — Track document staleness with freshness scores, alerts, and optional auto-review task creation
|
||||
- **Freshness headers** — YAML frontmatter with `fresh-days`, `owner`, `last-verified` fields
|
||||
- **Tracked metadata** — Registry records store review dates, owners, paths, thresholds, and tags without rewriting source documents
|
||||
- **Steward workflow** — Assigned doc owners responsible for periodic review
|
||||
- **Staleness API** — Query which docs need review based on freshness thresholds at `/api/doc-freshness`
|
||||
- **Configurable thresholds** — Set staleness thresholds via Settings → Doc Freshness
|
||||
- **3-phase automation** — Manual → scheduled checks → CI integration
|
||||
- **3-phase automation** — Manual registry review → scheduled checks → CI integration
|
||||
- **Inspired by** @mvoutov's BoardKit Orchestrator ("stale docs = hallucinating AI")
|
||||
|
||||
---
|
||||
|
|
@ -2343,13 +2343,13 @@ TRUST_PROXY=true
|
|||
|
||||
## Storage & Architecture
|
||||
|
||||
Abstract storage layer that decouples business logic from the filesystem.
|
||||
Deep storage modules decouple business logic from filesystem and SQLite details.
|
||||
|
||||
- **Repository pattern** — 5 repository interfaces abstract data access: `ActivityRepository`, `TemplateRepository`, `StatusHistoryRepository`, `ManagedListRepository`, `TelemetryRepository`
|
||||
- **StorageProvider** — Central provider extended with all repository implementations; services depend on interfaces, not filesystem calls
|
||||
- **`fs-helpers.ts`** — Centralized filesystem access module; the only file in the codebase that imports `fs` directly
|
||||
- **Service migration** — All 10 services migrated off direct `fs` imports to use the repository interfaces
|
||||
- **Extensibility** — Repository interfaces enable future storage backends (database, cloud storage) without changing service logic
|
||||
- **Repository contracts** — Persisted activity, progress, status history, deliverables, workflows, broadcasts, conflicts, delegation, ceremony, error analysis, permissions, lifecycle configuration, schedules, reflection, chat, tasks, telemetry, and managed content use explicit interfaces.
|
||||
- **File and SQLite parity** — Both backends preserve validated schemas, containment, locking, atomic mutation, pagination, and migration behavior appropriate to each domain.
|
||||
- **Service boundary gate** — Production services cannot introduce direct filesystem imports; authoritative reads and writes flow through the storage layer.
|
||||
- **Canonical runtime paths** — `DATA_DIR` and `VERITAS_DATA_DIR`, legacy discovery, backup, integrity, migration, health, and Docker mounts resolve through the same path contract.
|
||||
- **Extensibility** — Business services depend on domain operations instead of storage layout, allowing backend changes without duplicating product rules.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -2371,7 +2371,8 @@ Production-ready deployment and development tooling.
|
|||
|
||||
- **GitHub Actions** — CI pipeline on push to `main` and pull requests
|
||||
- **Concurrency control** — In-progress runs cancelled when new commits push
|
||||
- **Pipeline jobs** — Lint and warning budget, type check, workspace unit tests, production build, and security audit
|
||||
- **Fast pull-request jobs** — Source-policy selection, lint and warning budget, typecheck, production build, dependency audit, CodeQL, and gitleaks
|
||||
- **Milestone jobs** — Workspace tests, critical-path coverage, Playwright, desktop artifacts, load checks, and Docker contracts run for `ci:full`, scheduled, or manual milestones
|
||||
- **Scheduled QA** — Weekly and manually triggered Playwright and k6 gates run outside the fast PR path
|
||||
- **Release validation** — `pnpm validate:release` checks root/shared/server/web/CLI/MCP/desktop versions, the release-major document set, built artifacts, and optional GitHub tag/release state
|
||||
- **pnpm caching** — Dependency cache for faster CI runs
|
||||
|
|
@ -2397,12 +2398,14 @@ Production-ready deployment and development tooling.
|
|||
|
||||
## Testing
|
||||
|
||||
Multi-layer testing strategy.
|
||||
Multi-layer, milestone-scoped verification strategy. Exact release counts live
|
||||
in `docs/V6-RC-EVIDENCE-PACKET.md`; historical counts are not treated as current
|
||||
proof.
|
||||
|
||||
### Unit Tests (Vitest)
|
||||
|
||||
- **119 test files** · **1,699 tests passing** across server and frontend
|
||||
- **Server (105 files, 1,570 tests):**
|
||||
- **Workspace coverage** — Server, web, CLI, MCP, shared contracts, and desktop packages are included in the canonical release gate.
|
||||
- **Server coverage includes:**
|
||||
- All middleware (auth, rate limiting, request ID, API versioning, cache control, validation, response envelope, request timeout)
|
||||
- Core services (task, template, telemetry, notification, activity, sprint, diff, conflict, summary, status history, digest, attachment, text extraction, migration, managed list, broadcast, automation, blocking, failure alert, metrics, settings, JWT rotation, MIME validation, preview, trace, circuit breaker)
|
||||
- Route handlers (tasks, task archive, task comments, task subtasks, task time, auth, agent status, automation, config, notifications, templates, health, misc routes)
|
||||
|
|
@ -2411,7 +2414,7 @@ Multi-layer testing strategy.
|
|||
- Prometheus metrics (counters, gauges, histograms, registry, collector middleware)
|
||||
- Environment variable validation
|
||||
- Circuit breaker transitions (18 tests covering open/half-open/closed states — added in v3.3.2)
|
||||
- **Frontend (14 files, 129 tests):**
|
||||
- **Frontend coverage includes:**
|
||||
- API client helpers and task operations
|
||||
- Custom hooks: useWebSocket, useKeyboard (keyboard shortcuts)
|
||||
- Components: KanbanBoard, TaskCard, ErrorBoundary, AgentStatusIndicator, WebSocketIndicator
|
||||
|
|
@ -2420,9 +2423,8 @@ Multi-layer testing strategy.
|
|||
|
||||
### End-to-End Tests (Playwright)
|
||||
|
||||
- **7 spec files** covering critical user flows
|
||||
- **19/19 tests passing**
|
||||
- **Test suites:**
|
||||
- **Chromium and WebKit projects** cover critical user flows at declared QA and release milestones.
|
||||
- **Test suites include:**
|
||||
- Health check
|
||||
- Settings management
|
||||
- Task creation
|
||||
|
|
@ -2523,7 +2525,9 @@ Define scoring profiles with weighted criteria and evaluate agent outputs agains
|
|||
|
||||
**Key capabilities:**
|
||||
|
||||
- Four scorer types: `RegexMatch`, `KeywordContains`, `NumericRange`, `CustomExpression`
|
||||
- Four bounded scorer types: `RegexMatch`, `KeywordContains`, `NumericRange`, `OccurrenceRatio`
|
||||
- Regex evaluation runs outside the server event loop with input, pattern, and time limits
|
||||
- Occurrence ratios use literal values and optional numeric normalization; arbitrary code is never evaluated
|
||||
- Weighted scorers with optional `target`: `action`, `output`, or `combined`
|
||||
- Composite scoring methods: `weightedAvg`, `minimum`, `geometricMean`
|
||||
- Per-evaluation history with scorer-level breakdowns
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ A working board is not the same as agent-ready or external wake/delivery-ready.
|
|||
|
||||
| What | Command | Notes |
|
||||
| ----------------- | ------------------ | ----------------------------------------------------------------------- |
|
||||
| Node.js | `node -v` | Requires **22+**. Install via Volta/nvm if older. |
|
||||
| pnpm | `pnpm -v` | Requires **11.1.1+**. Prefer `corepack prepare pnpm@11.1.1 --activate`. |
|
||||
| Git | `git --version` | Any current version works. |
|
||||
| Node.js | `node -v` | Requires **22.22.1+**. Install via Volta/nvm if older. |
|
||||
| pnpm | `pnpm -v` | Use the repository-pinned **11.1.1** release. |
|
||||
| Git | `git --version` | Requires **2.38+**. |
|
||||
| (Optional) Docker | `docker --version` | Needed only if you prefer containers. |
|
||||
|
||||
That's it. No database, no extra services.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ Every project should maintain these files:
|
|||
| `prompt-registry/*.md` | Workflow prompts | When prompts drift or improve |
|
||||
| `README.md` | Project overview, quick start | After major releases |
|
||||
|
||||
Register maintained living documents in Settings → Doc Freshness. The registry
|
||||
record, not an optional Markdown comment, is authoritative for the last review,
|
||||
reviewer, maximum age, score, and alerts. Historical evidence and release notes
|
||||
do not need synthetic freshness headers.
|
||||
|
||||
### Optional Model-Specific Files
|
||||
|
||||
- `GPT.md` — GPT-specific notes (if behavior differs from Claude)
|
||||
|
|
@ -45,7 +50,9 @@ Update docs **within the same session** when:
|
|||
|
||||
1. **A bug was caused by missing context** — Add durable shared context to
|
||||
`AGENTS.md`, or a harness-specific supplement when it truly differs
|
||||
2. **Cross-model review catches a pattern** — Document the pattern
|
||||
2. **Focused review catches a pattern** — Document the pattern regardless of
|
||||
whether the reviewer is a maintainer, an independent agent, or a configured
|
||||
governance gate
|
||||
3. **A workaround is discovered** — Add it to Troubleshooting or the nearest
|
||||
applicable instruction file
|
||||
4. **API behavior changes** — Update relevant docs
|
||||
|
|
@ -86,7 +93,8 @@ Run this monthly or after major releases:
|
|||
|
||||
- [ ] Prompts reference current API endpoints
|
||||
- [ ] No prompts for removed features
|
||||
- [ ] Cross-model review prompt matches current checklist
|
||||
- [ ] Optional review prompts match the current checklist and are not described
|
||||
as default delivery gates
|
||||
|
||||
### README.md
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,26 @@ The Scoring Framework lets you define profiles with weighted criteria that evalu
|
|||
|
||||
**Scorer types:**
|
||||
|
||||
| Type | What it checks |
|
||||
| ------------------- | ------------------------------------------------------------ |
|
||||
| `RegexMatch` | Whether the output matches a regular expression |
|
||||
| `KeywordContains` | Whether the output contains required keywords |
|
||||
| `NumericRange` | Whether a numeric field in the output falls within a range |
|
||||
| `CustomExpression` | A custom evaluation expression |
|
||||
| Type | What it checks |
|
||||
| ------------------ | --------------------------------------------------------------------- |
|
||||
| `RegexMatch` | Whether bounded worker-isolated regex evaluation matches |
|
||||
| `KeywordContains` | Whether the output contains required keywords |
|
||||
| `NumericRange` | Whether a numeric field in the output falls within a range |
|
||||
| `OccurrenceRatio` | Literal occurrence density with optional numeric-path normalization |
|
||||
|
||||
`RegexMatch` accepts patterns up to 256 characters and any valid JavaScript regex flag set supported
|
||||
by the active Node runtime. Evaluation uses a globally bounded four-worker pool outside the server
|
||||
event loop, a bounded wait queue, and a 100 ms limit. Output is limited to 100,000 characters,
|
||||
action text to 10,000 characters, and their combined scoring target to 110,001 characters.
|
||||
|
||||
`OccurrenceRatio` is the declarative replacement for legacy custom expressions. It accepts one to
|
||||
32 literal `needles` and divides their occurrence count by either a fixed `denominator` or a numeric
|
||||
`denominatorPath`, optionally scaled with `denominatorScale`. `wholeWord`, `caseSensitive`,
|
||||
`minimumDenominator`, and `invert` provide bounded transformations without executing code.
|
||||
|
||||
Persisted profiles containing the removed `CustomExpression` scorer fail closed during evaluation.
|
||||
Replace those scorers through the profile API before retrying; the server never evaluates or
|
||||
silently converts the stored expression.
|
||||
|
||||
**Composite methods:**
|
||||
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ Example frontmatter:
|
|||
id: cross-model-review
|
||||
name: Cross Model Review
|
||||
category: evaluation
|
||||
description: Opposite-model review checklist
|
||||
description: Optional independent review checklist
|
||||
---
|
||||
|
||||
# Cross Model Review
|
||||
|
|
|
|||
|
|
@ -134,13 +134,17 @@ Publish shared resources as a package:
|
|||
|
||||
## What to Share
|
||||
|
||||
### Always Share
|
||||
### Common Shared Resources
|
||||
|
||||
- Cross-model review prompt (consistency is critical)
|
||||
- Security review checklist
|
||||
- Definition of Done template
|
||||
- Sprint planning prompt
|
||||
|
||||
### Share Only When Configured
|
||||
|
||||
- Optional independent-review prompt when the consuming workspace explicitly
|
||||
enables that workflow
|
||||
|
||||
### Share Carefully
|
||||
|
||||
- Agent personality (AGENTS.md) — may need project-specific tweaks
|
||||
|
|
@ -236,7 +240,7 @@ Fixture contracts live in
|
|||
```markdown
|
||||
## Instructions
|
||||
|
||||
Follow the standard code review process.
|
||||
Follow the configured independent review process.
|
||||
See prompt: `prompt-registry/cross-model-review.md`
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ Example: 6 tasks × 4 subtasks × 0.5d = 12 agent-days. With 3 agents @ 4 days f
|
|||
| US-1602: Task Workflow SOP | docs | high | Defines lifecycle. |
|
||||
| US-1603: Sprint Planning SOP | docs | medium | This document. |
|
||||
| US-1604: Multi-Agent Orchestration | docs | medium | PM + workers. |
|
||||
| US-1605: Cross-Model Review | docs | medium | Opposite model gate. |
|
||||
| US-1605: Review Policy | docs | medium | Optional review criteria. |
|
||||
| US-1606: Best Practices | docs | medium | Patterns + anti-patterns. |
|
||||
|
||||
Clone this pattern for your own projects; rename sprint `US-YYYY` and fill tasks accordingly.
|
||||
|
|
|
|||
|
|
@ -174,7 +174,9 @@ Thresholds (hardcoded in v4.0):
|
|||
|
||||
**Status shows `elevated` with all agents appearing online:** Check the operations signal — `status: critical` also triggers `elevated`. The agent registry shows registered agents, not process health.
|
||||
|
||||
**`system.disk: false` immediately after startup:** The data directory path may be wrong. Check the `DATA_DIR` environment variable — it should point to the `.veritas-kanban` data directory.
|
||||
**`system.disk: false` immediately after startup:** The storage root may be wrong. Check
|
||||
`DATA_DIR` (or `VERITAS_DATA_DIR` when `DATA_DIR` is unset); runtime health checks use its
|
||||
`.veritas-kanban` child directory.
|
||||
|
||||
**Health endpoint returns 500:** The metrics service or agent registry service failed to initialize. Check the server startup logs.
|
||||
|
||||
|
|
|
|||
|
|
@ -141,22 +141,23 @@ pnpm install
|
|||
pnpm build
|
||||
```
|
||||
|
||||
If errors persist, check your Node.js version — **Node 22+** is required:
|
||||
If errors persist, check your Node.js version. **Node 22.22.1+** is required:
|
||||
|
||||
```bash
|
||||
node -v # Should be v22.x or higher
|
||||
node -v # Must be v22.22.1 or higher
|
||||
```
|
||||
|
||||
### `pnpm` not found
|
||||
|
||||
Veritas Kanban uses pnpm workspaces. Install it first:
|
||||
Veritas Kanban uses pnpm workspaces. Activate the repository-pinned version:
|
||||
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
# or
|
||||
corepack enable && corepack prepare pnpm@latest --activate
|
||||
corepack enable
|
||||
corepack prepare pnpm@11.1.1 --activate
|
||||
```
|
||||
|
||||
Do not install this workspace with npm, Yarn, or Bun.
|
||||
|
||||
### Port already in use
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# Veritas Kanban v6 Compatibility And Release Policy
|
||||
|
||||
This policy defines supported v6.1.1 combinations, harness evidence, release
|
||||
This policy defines supported v6.1.2 combinations, harness evidence, release
|
||||
channels, and rollback limits. The machine-readable harness record at
|
||||
`GET /api/config/harness-compatibility` is authoritative for exact capability
|
||||
digests, fixture revisions, and the current host's live state.
|
||||
|
||||
Documentation freshness: 2026-08-22 for Veritas Kanban 6.1.1.
|
||||
Documentation freshness: 2026-08-24 for Veritas Kanban 6.1.2.
|
||||
|
||||
## Harness Support Tiers
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ are incompatible with v6.
|
|||
|
||||
| Component | Supported v6 combination | Detection/evidence | Fail-closed boundary |
|
||||
| -------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Server, web, shared, CLI, MCP, desktop | All release packages are exactly 6.1.1. | Package manifests, `/api/health.version`, `vk --version`, MCP metadata, desktop bundle/update metadata. | Mixed release packages are unsupported for publication. |
|
||||
| Server, web, shared, CLI, MCP, desktop | All release packages are exactly 6.1.2. | Package manifests, `/api/health.version`, `vk --version`, MCP metadata, desktop bundle/update metadata. | Mixed release packages are unsupported for publication. |
|
||||
| Public API | REST API remains `v1` at `/api/v1`, with `/api` compatibility aliases where documented. | `X-API-Version`, OpenAPI/reference docs, CLI/MCP smoke. | Unknown API versions or incompatible auth fail before mutation. |
|
||||
| Buzz Agent | Buzz v0.4.24 commit `710ed9fff57878a1d69f809b80a6ee0416c53fc4`; `buzz-agent 0.1.0`; ACP v1. | Exact initialize identity, capability digest, probe revision, composed Buzz fixtures. | Unknown build, `buzz-acp`, resume, HTTP/SSE MCP, or capability drift blocks. |
|
||||
| Buzz relay integration | Buzz v0.4.24; NIP-11, NIP-29, NIP-42; optional NIP-43 membership. | Pinned relay compatibility evidence, signed query/event fixtures, mapping state. | Host/TLS drift, unsafe URL, bad signature, identity mismatch, replay, or disabled mapping blocks. |
|
||||
|
|
@ -37,7 +37,7 @@ are incompatible with v6.
|
|||
| GitHub Copilot CLI | v1.0.74 public-preview ACP; tag commit `2b809c84e87dbcc88f897cb4f3fb97c43b77af95`. | Version and ACP initialize handshake; authentication remains provider-managed. | Version drift, broad allow, remote/plugin/config injection, or unsupported controls blocks. |
|
||||
| Hermes Agent | v2026.7.7.2 one-shot process adapter. | `hermes --version` and allowlisted boot authentication. | Resume/follow-up remains unsupported. |
|
||||
| OpenClaw | v2026.6.11 gateway adapter. | Gateway health, runtime manifest, explicit operator tool policy. | Missing `sessions_spawn`/`sessions_send`, unknown evidence, or unsupported task controls blocks. |
|
||||
| macOS desktop | macOS arm64 signed/notarized app with bundled 6.1.1 server/web. | Bundle version, signature, Gatekeeper, stapling, `/api/health.version`, update metadata. | Mixed bundle/runtime, failed readiness, signature, or metadata checks blocks stable publication. |
|
||||
| macOS desktop | macOS arm64 signed/notarized app with bundled 6.1.2 server/web. | Bundle version, signature, Gatekeeper, stapling, `/api/health.version`, update metadata. | Mixed bundle/runtime, failed readiness, signature, or metadata checks blocks stable publication. |
|
||||
| Linux/Windows desktop | Unsigned preview artifacts only. | Cross-platform packaging workflows. | Not a supported stable install or update channel. |
|
||||
| Desktop SQLite/profile | Existing v5.2.5 workspace upgraded in place after a complete backup. | Data/profile counts, integrity check, startup normalization, board/runtime smoke. | Competing writers, unsafe filesystem, failed migration, or missing recovery evidence blocks acceptance. |
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,39 @@
|
|||
# Veritas Kanban v6 GA Checklist
|
||||
|
||||
This checklist contains the active stable-release gate for Veritas Kanban
|
||||
6.1.1 and retains the completed 6.1.0 and 6.0.2 evidence below. Command results, platform
|
||||
6.1.2 and retains the completed 6.1.1, 6.1.0, and 6.0.2 evidence below. Command results, platform
|
||||
details, workflow links, limitations, and artifact hashes belong in
|
||||
[v6 Release Candidate Evidence Packet](V6-RC-EVIDENCE-PACKET.md).
|
||||
|
||||
Documentation freshness: 2026-08-22 for Veritas Kanban 6.1.1.
|
||||
Documentation freshness: 2026-08-24 for Veritas Kanban 6.1.2.
|
||||
|
||||
## 6.1.1 Release Gate
|
||||
## 6.1.2 Release Gate
|
||||
|
||||
- [x] Audit issues #1162-#1173 are closed through merged, evidence-linked pull
|
||||
requests and the single final regression milestone.
|
||||
- [x] Root, shared, server, web, CLI, MCP, and desktop manifests are 6.1.2.
|
||||
- [x] README, canonical instructions, API reference, compatibility policy,
|
||||
upgrade guide, release notes, canonical GitHub body, freshness record, and
|
||||
changelog are synchronized for 6.1.2.
|
||||
- [x] Runtime paths, storage repositories, provider adapters and lifecycle,
|
||||
credential-aware frontend requests, immutable actions, continuous
|
||||
scanning, critical coverage, dependency cleanup, lint ratchets, and the
|
||||
production Docker contract are represented in release documentation.
|
||||
- [x] Independent and cross-model review remain optional; they are not part of
|
||||
the default delivery or release gate.
|
||||
- [x] The coordinated security fix is integrated, released in supported
|
||||
artifacts, and published through the approved repository advisory.
|
||||
- [x] One clean final candidate passes the complete Node-floor and current-Node
|
||||
verification matrix with exact counts, skips, retries, image size, and
|
||||
limitations recorded in the evidence packet.
|
||||
- [x] The release PR merges and its exact merge is published as annotated
|
||||
`v6.1.2` with a live body matching `docs/releases/v6.1.2.md`.
|
||||
- [x] Signed/notarized macOS assets, updater metadata, installed-app readiness,
|
||||
the live Homebrew cask, and the advisory disposition are verified.
|
||||
- [x] Every publication readback required before closing release tracker #1174
|
||||
has passed; close the tracker after this evidence update merges.
|
||||
|
||||
## Historical 6.1.1 Completed Release Gate
|
||||
|
||||
- [x] Issue #1153 and pull requests #1148, #1149, #1150, #1154, and #1155
|
||||
received an evidence-backed maintainer disposition.
|
||||
|
|
@ -26,9 +52,9 @@ Documentation freshness: 2026-08-22 for Veritas Kanban 6.1.1.
|
|||
- [x] Independent review is owner-directed and is not part of the active
|
||||
6.1.1 release gate; exact local and CI evidence carries the release
|
||||
decision.
|
||||
- [ ] The release PR merges and the exact merge is published as annotated
|
||||
- [x] The release PR merges and the exact merge is published as annotated
|
||||
`v6.1.1` with a live body matching `docs/releases/v6.1.1.md`.
|
||||
- [ ] Signed/notarized macOS assets, updater metadata, independent installed-app
|
||||
- [x] Signed/notarized macOS assets, updater metadata, independent installed-app
|
||||
readiness, and the Homebrew cask are published and verified.
|
||||
|
||||
## Historical 6.1.0 Completed Release Gate
|
||||
|
|
@ -55,37 +81,57 @@ Documentation freshness: 2026-08-22 for Veritas Kanban 6.1.1.
|
|||
|
||||
## Final Release Validation Commands
|
||||
|
||||
Run once from the clean 6.1.1 release candidate:
|
||||
Apply `ci:full` to the release pull request and keep it applied through the
|
||||
final candidate synchronization. That single milestone runs the complete
|
||||
workspace suite, critical-path coverage, unsigned desktop artifacts, and
|
||||
Docker image contract. Run the following commands once from the clean 6.1.2
|
||||
release candidate at the supported Node floor and current supported Node:
|
||||
|
||||
```bash
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm check:pnpm-settings
|
||||
pnpm check:security-artifacts
|
||||
pnpm check:delivery-cadence
|
||||
pnpm test:ci-scope
|
||||
pnpm audit --prod --audit-level=high
|
||||
pnpm audit:all
|
||||
pnpm check:gitleaks
|
||||
pnpm lint
|
||||
pnpm lint:budget
|
||||
pnpm lint:report
|
||||
pnpm qa:mantine
|
||||
pnpm typecheck
|
||||
pnpm build
|
||||
pnpm test
|
||||
pnpm test:unit
|
||||
pnpm test:e2e
|
||||
pnpm smoke:cli-mcp
|
||||
pnpm test:buzz:compatibility
|
||||
pnpm desktop:test
|
||||
pnpm desktop:build
|
||||
pnpm desktop:check:electron-artifacts
|
||||
pnpm desktop:test:readiness
|
||||
pnpm desktop:dev:fresh
|
||||
pnpm desktop:smoke:mac:local
|
||||
pnpm desktop:package:mac:unsigned
|
||||
pnpm validate:release -- --version 6.1.1
|
||||
pnpm validate:release -- --version 6.1.1 --docker-build
|
||||
pnpm test:release-format
|
||||
pnpm validate:release -- --version 6.1.2 --skip-build-output
|
||||
pnpm validate:release -- --version 6.1.2 --docker-build
|
||||
```
|
||||
|
||||
Mount and inspect the unsigned DMG and ZIP, exercise the visible native
|
||||
single-instance/reopen/clean-close/quit lifecycle with an isolated profile, and
|
||||
run the production image as its non-root user against an isolated volume.
|
||||
Record health, auth, SQLite, static-web, canonical-path, backup, integrity,
|
||||
image-size, and clean-shutdown evidence. The same candidate must pass these
|
||||
gates at Node 22.22.1 and the current supported Node runtime.
|
||||
|
||||
## Distribution And Post-Publication
|
||||
|
||||
Publish only after every pre-publication 6.1.1 gate above is checked. The canonical
|
||||
GitHub body is `docs/releases/v6.1.1.md`; post-publication validation must run
|
||||
`pnpm validate:release -- --version 6.1.1 --github --repo BradGroux/veritas-kanban`.
|
||||
Update the Homebrew cask only from the independently verified published ZIP
|
||||
checksum.
|
||||
The 6.1.2 publication gate is complete. The final candidate, release merge,
|
||||
annotated tag, signed/notarized artifacts, independent launch verification,
|
||||
post-publication validator, live Homebrew cask, and approved advisory
|
||||
disposition are verified in the evidence packet. Completed 6.1.1 evidence
|
||||
remains recorded below.
|
||||
|
||||
## Historical 6.0.2 Source And Scope
|
||||
|
||||
|
|
@ -130,8 +176,9 @@ checksum.
|
|||
- [x] Native About, copied support information, the desktop bridge, and updater
|
||||
fallback consume one authoritative version/build/channel/OS/architecture
|
||||
record (#1005).
|
||||
- [x] Pull-request verification uses documentation-only, focused, or full test
|
||||
scope, while explicit and milestone release gates remain full (#1000).
|
||||
- [x] Ordinary pull-request verification records affected workspaces without
|
||||
running tests; manual focused diagnostics and explicit `ci:full`,
|
||||
scheduled, or release milestones own the test suites (#1000, #1227).
|
||||
- [x] Published release notes are sourced from
|
||||
`docs/releases/vX.Y.Z.md`, use one full-width Markdown line per paragraph
|
||||
or list item, reject blockquotes and overlong prose blocks, and are
|
||||
|
|
|
|||
|
|
@ -1,15 +1,89 @@
|
|||
# Veritas Kanban v6 Release Candidate Evidence Packet
|
||||
|
||||
This packet records the Veritas Kanban 6.1.1 maintenance release candidate and
|
||||
retains historical evidence for the completed 6.1.0 release, the quarantined 6.0.0 prerelease, the 6.0.1
|
||||
This packet records the active Veritas Kanban 6.1.2 audit release candidate and
|
||||
retains historical evidence for the completed 6.1.1 and 6.1.0 releases, the quarantined 6.0.0 prerelease, the 6.0.1
|
||||
stabilization release, and the 6.0.2 desktop recovery hotfix. It separates
|
||||
merged implementation, deterministic conformance, local runtime proof, signed
|
||||
publication, and Homebrew availability.
|
||||
|
||||
Veritas Kanban 6.1.0 remains the supported stable v6 release until 6.1.1
|
||||
publication and artifact verification complete. Do not use 6.0.0 for installation or upgrade validation.
|
||||
Veritas Kanban 6.1.2 is the supported stable v6 release. Do not use 6.0.0 for
|
||||
installation or upgrade validation.
|
||||
|
||||
Documentation freshness: 2026-08-22 for the Veritas Kanban 6.1.1 release candidate.
|
||||
Documentation freshness: 2026-08-24 for the published Veritas Kanban 6.1.2 release.
|
||||
|
||||
## 6.1.2 Audit Release Candidate
|
||||
|
||||
| Field | Value |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Release version | 6.1.2 |
|
||||
| Source branch | `release/6.1.2-audit` |
|
||||
| Source baseline | `3851fea93ecfe5119e4092739662443d29059ac7`, `main` after release-gate fix PR #1243 |
|
||||
| Validated candidate | `2a21178df97a00395cfc6c43774a57496a5c1f6a`, the frozen release PR head before this evidence-only update |
|
||||
| Included issues | Audit tracker [#1174](https://github.com/BradGroux/veritas-kanban/issues/1174), closed findings #1162-#1173, and closed CodeQL baseline #1231 |
|
||||
| Public implementation | #1162-#1173 are closed through merged work. Coordinated remediation and release-gate fixes are merged through #1236, #1239, #1241, and #1243 |
|
||||
| Security disposition | Remediation is integrated into supported 6.1.2 artifacts. The approved [repository security advisory](https://github.com/BradGroux/veritas-kanban/security/advisories/GHSA-4r99-qpvh-wrqf) was published after artifact verification |
|
||||
| Publication state | Complete. Release PR #1237, annotated `v6.1.2`, the stable GitHub release, signed/notarized assets, updater metadata, post-publication validator, Homebrew cask/install, and advisory disposition are verified |
|
||||
|
||||
### 6.1.2 issue and pull request traceability
|
||||
|
||||
| Phase | Issue | Merged evidence |
|
||||
| -------------------- | --------------------------------------------------------------- | -------------------------- |
|
||||
| Verification | #1172 deterministic and milestone-scoped test gates | #1175, #1177, #1181, #1228 |
|
||||
| Verification | #1171 native-loader Vite/Vitest configuration | #1178 |
|
||||
| Supply chain | #1167 immutable actions | #1179 |
|
||||
| Security gates | #1168 continuous scanning | #1180 |
|
||||
| Coverage | #1169 critical-path baselines and ratchets | #1183 |
|
||||
| Runtime paths | #1162 canonical `DATA_DIR` behavior | #1184 |
|
||||
| Persistence | #1163 storage boundary restoration | #1190-#1220 |
|
||||
| Provider runtime | #1164 lifecycle and provider decomposition | #1223-#1230 |
|
||||
| Frontend API | #1165 credential-aware requests | #1218 |
|
||||
| Dependencies | #1170 unused direct dependencies | #1217 |
|
||||
| Container | #1166 production runtime and size contract | #1222 |
|
||||
| Type safety | #1173 lint-debt ratchet | #1221 |
|
||||
| CodeQL baseline | #1231 initial alert triage, remediation, and disposition | #1232-#1235 |
|
||||
| Coordinated security | Private release blocker integrated without premature disclosure | #1236 |
|
||||
| Release validation | Recovery-key alphabet and WebSocket header forwarding | #1238, #1239 |
|
||||
| Release validation | Same-task lifecycle invocation ordering | #1240, #1241 |
|
||||
| Release validation | Sanitized URI prefix validation | #1242, #1243 |
|
||||
|
||||
The initial CodeQL baseline contained 195 open alerts. All were reviewed: 67
|
||||
were closed through source remediation and 128 received specific,
|
||||
evidence-backed dispositions. The post-merge default-branch Security Gates run
|
||||
[`32700390853`](https://github.com/BradGroux/veritas-kanban/actions/runs/32700390853)
|
||||
completed successfully at `1cdcd6ec60e3f48b6017146b2583fa82f7061c68`
|
||||
with zero open alerts.
|
||||
|
||||
The 2026-08-24 pre-release and post-publication GitHub security readbacks
|
||||
confirm Dependabot security updates, secret scanning, and push protection are
|
||||
enabled, and open Dependabot, secret-scanning, and default-branch CodeQL alert
|
||||
counts are all zero. Every external workflow action reference is pinned to a
|
||||
full commit SHA. The final readback was taken after the exact-main Security
|
||||
Gates run passed.
|
||||
|
||||
Final-milestone preflight on 2026-08-24 found local Node 26.7.0, pnpm 11.1.1,
|
||||
Git 2.55.0, and an available Docker 29.2.1 server. The Node 22 floor remains
|
||||
the `ci:full` runner gate; no separate local Node 22 installation is present.
|
||||
The required macOS signing secret names and the complete App Store Connect
|
||||
notarization secret-name set are configured, without reading their values. The
|
||||
validation host had no existing Veritas Kanban app or cask before publication.
|
||||
The live Homebrew install therefore replaced no active application or user data.
|
||||
|
||||
### 6.1.2 verification matrix
|
||||
|
||||
The final matrix ran once on the fully integrated candidate. The checked-in
|
||||
evidence update is documentation-only and does not alter the validated runtime.
|
||||
|
||||
| Gate | Environment | Candidate result |
|
||||
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Frozen install, package-manager, security-artifact, delivery-cadence, and CI-scope policy | Local Node 26.7.0 and CI Node 22; pnpm 11.1.1 | Pass. Frozen install completed; package-manager, 1,864-file security-artifact, 18/18 delivery-cadence, and 18/18 CI-scope gates passed |
|
||||
| Typecheck, lint, 458-warning budget, lint report, production/full audit, and gitleaks | Local Node 26.7.0 and CI Node 22 | Pass. Typecheck and lint completed with zero errors and the exact 458-warning budget; production and full audits found no known vulnerabilities; gitleaks passed |
|
||||
| Workspace and orchestration units | Local clean worktree and [CI run 32732019845](https://github.com/BradGroux/veritas-kanban/actions/runs/32732019845) | Pass. Local workspace packages reported 4,289 passed and 24 skipped, plus 3/3 root orchestration tests. CI independently reported server 3,376 passed/5 skipped, web 780 passed, CLI 62 passed, MCP 71 passed/19 skipped, and dual-storage parity 4/4 |
|
||||
| Critical-path coverage ratchets | CI Node 22 on frozen candidate | Pass. All seven boundaries passed: server dispatch 65.2% lines, auth 59.05%, storage 50.45%, web 55.25%, CLI 52.07%, MCP 52.39%, and desktop 66.24%; [coverage artifact](https://github.com/BradGroux/veritas-kanban/actions/runs/32732019845/artifacts/9521857932) SHA-256 `395ae4aef57b8dcb24eabe2ad2ac628f8bbe51564195b7525e5c2be57613a096` |
|
||||
| Playwright Chromium and WebKit | [Scheduled QA run 32732019821](https://github.com/BradGroux/veritas-kanban/actions/runs/32732019821) | Pass. 37/37 cases passed in 3.6 minutes with zero retries; k6 completed 7/7 checks, 5 requests, zero request failures, and one uninterrupted smoke iteration |
|
||||
| Build, Mantine QA, CLI/MCP smoke | Local clean worktree and CI Node 22 | Pass. Build and Mantine QA passed; initial JS/CSS were 242.3/53.7 KiB gzip. CLI/MCP compatibility had zero failures or warnings; two live read/write checks were explicitly skipped because the isolated profile had no `VK_API_KEY` |
|
||||
| Desktop tests, build, readiness, native lifecycle, and unsigned package | macOS arm64 isolated profile and [artifact run 32732019898](https://github.com/BradGroux/veritas-kanban/actions/runs/32732019898) | Pass. Desktop 67/67, Electron artifacts 4/4, readiness 7/7, package smoke, visible setup/readiness, single-instance, close/reopen, and clean quit all passed. Mounted DMG and ZIP report 6.1.2 arm64. DMG: 265,821,857 bytes, SHA-256 `562aa08c1d93653227aa0deee7cc0020f42bfe4ead9e404005fbe67a87822d7a`; ZIP: 270,676,980 bytes, SHA-256 `e1dc99f95e1c3396cda78c5e38582cdc7554baf2757aa57cfee411ba6a94de60`. CI macOS/Linux/Windows unsigned artifacts all passed |
|
||||
| Production Docker build, image-size contract, and runtime smoke | amd64 [Docker contract run 32732019831](https://github.com/BradGroux/veritas-kanban/actions/runs/32732019831) | Pass. Image size 571,628,184 bytes, below 600,000,000; non-root user, version, mounted paths, SQLite, backup, auth, static web, health, bcrypt, and clean shutdown passed |
|
||||
| Release-format and release validators | Version 6.1.2 | Pass with one classified host limitation. Canonical release format passed 3/3 and every source/build-output check passed. Post-publication GitHub, tag, and body validation passed. The local Docker-enabled wrapper reached image assembly before Docker Desktop returned an `overlayfs` containerd metadata I/O error on a full host filesystem; the clean CI Docker build and complete runtime contract passed on the same frozen source |
|
||||
|
||||
## 6.1.1 Maintenance Release Candidate
|
||||
|
||||
|
|
@ -20,7 +94,7 @@ Documentation freshness: 2026-08-22 for the Veritas Kanban 6.1.1 release candida
|
|||
| Source baseline | `main` after the audited task-drawer fix, dependency rollup, and Chalk 6 disposition (#1154, #1155, #1149) |
|
||||
| Included issues | [#1153](https://github.com/BradGroux/veritas-kanban/issues/1153) and release tracker [#1156](https://github.com/BradGroux/veritas-kanban/issues/1156) |
|
||||
| Pull-request audit | #1154 accepted and merged with browser proof; #1155 accepted with refreshed security floors; #1149 accepted after Node.js and test review; #1150 closed as unnecessary because `pnpm/action-setup@v6` already resolves to 6.0.9; #1148 rejected because its engine floor and 65 failing tests violate the release contract |
|
||||
| Publication state | Pending the consolidated release gate, release PR, annotated tag, GitHub publication, signed/notarized assets, installed-app verification, and Homebrew cask update |
|
||||
| Publication state | Complete. PR #1157 merged as `2cfb89396da7e115571f3c1449449ef60bde53d7`; annotated `v6.1.1`, the live GitHub release body, signed/notarized assets, installed-app verification, and Homebrew PR #49 are verified. |
|
||||
|
||||
### 6.1.1 verification evidence
|
||||
|
||||
|
|
@ -318,6 +392,48 @@ test.
|
|||
No private data, credential value, raw provider conversation, or unrestricted
|
||||
runtime profile is retained in this packet.
|
||||
|
||||
## 6.1.2 Publication Evidence
|
||||
|
||||
Source publication, signed-macOS verification, full-width release-note
|
||||
validation, isolated installed-app readiness, Homebrew distribution, and the
|
||||
approved advisory disposition are complete.
|
||||
|
||||
| Publication item | Result |
|
||||
| --- | --- |
|
||||
| Release PR and merge | [#1237](https://github.com/BradGroux/veritas-kanban/pull/1237); frozen full-matrix head `2a21178df97a00395cfc6c43774a57496a5c1f6a`; final evidence-only head `ec1d3a7e106e2dd2753d41b5e351cdf665e2cd7c`; verified squash merge `dfae7911cc282e32262a006e2171ce5fe4865714` |
|
||||
| Annotated `v6.1.2` tag object and peeled commit | `819aad9ae8eae3f2f40593d4567647963f7bfbc3`; `dfae7911cc282e32262a006e2171ce5fe4865714` |
|
||||
| GitHub release URL and body | [Veritas Kanban 6.1.2](https://github.com/BradGroux/veritas-kanban/releases/tag/v6.1.2); stable release published 2026-08-24; the live body exactly matches `docs/releases/v6.1.2.md` |
|
||||
| Exact-main CI and security | [CI run 32734012479](https://github.com/BradGroux/veritas-kanban/actions/runs/32734012479) and [Security Gates run 32734012461](https://github.com/BradGroux/veritas-kanban/actions/runs/32734012461) passed on the exact release merge; CodeQL, Dependabot, secret scanning, push protection, gitleaks, and immutable-action gates have no unresolved release blockers |
|
||||
| Desktop Release workflow | [Run 32734749604](https://github.com/BradGroux/veritas-kanban/actions/runs/32734749604); exact release merge SHA; signed and notarized macOS job passed in 11m1s |
|
||||
| Signed/notarized DMG | `Veritas-Kanban-6.1.2-mac-arm64.dmg`; 267,430,041 bytes; SHA-256/GitHub digest and published sidecar value `1b2a75c241642a8af14e48827dfc48c5e7846f2709af2359dc1ae34ba2588baa`; Apple notarization accepted, stapling validated, and Gatekeeper accepted Notarized Developer ID `RLBHD62MPW` |
|
||||
| Signed/notarized ZIP | `Veritas-Kanban-6.1.2-mac-arm64.zip`; 271,666,610 bytes; SHA-256/GitHub digest and published sidecar value `81ea146d20d2ab279331e73c01bc3c4eafdda8a4082be3607e9ca535a7d2be85`; the independently downloaded Homebrew cache matched this digest and contained bundle version 6.1.2 |
|
||||
| DMG/ZIP blockmaps and SHA-256 sidecars | Blockmap digests `9c99a416dbc518306456bf7025cbe478ea1242a40eb8acf349eb3dc67946ee97` and `77e55eb209f04b10bc1d38e2600032a1b2e808b4eeb11389d9e02819d306c935`; sidecar-file digests `30a6aa1831ed8f8f9c93c0e7e705c5f5e18805d092033732352f4513b6077c5a` and `9608ca6f5b365d9f74d0e6f9b3f71164dee4212af136b8cc8adf5650908b9790` |
|
||||
| `latest-mac.yml` | Version 6.1.2; 530 bytes; SHA-256 `591073ea888481ffdf1b0d2dc1d026b751422d44954ff2084bf7f8e64151992b`; ZIP and DMG names, sizes, and SHA-512 values match the published assets |
|
||||
| Installed signed-app isolated launch | Homebrew installed 6.1.2 without replacing an existing app or cask. `CFBundleShortVersionString` and `CFBundleVersion` report 6.1.2; deep strict code-signature validation, hardened runtime, Gatekeeper acceptance, and stapling validation pass. A disposable user-data root launched the installed app on isolated port 3101; exact-version readiness passed in 209ms and proved the packaged app owned the listener. `/api/health` reported 6.1.2, first-run onboarding showed all local readiness checks healthy, native menus rendered, and the task-owned process tree and listener stopped without touching the separate development build |
|
||||
| Release validator | Post-publication `pnpm validate:release -- --version 6.1.2 --github --repo BradGroux/veritas-kanban` passes and proves the live release and body match the annotated tag and canonical checked-in file |
|
||||
| Homebrew cask | [Issue #50](https://github.com/BradGroux/homebrew-tap/issues/50); [PR #51](https://github.com/BradGroux/homebrew-tap/pull/51); reviewed head `a3269d43eb657c0480e549e4d1fc15bc21baddea`; verified merge `2b29b79a26b269ed832f38b7173c7459c3db508d`; registered `bradgroux/tap/veritas-kanban` resolves 6.1.2 with the published ZIP checksum and passes Ruby syntax, cask style, strict online audit, dry-run install, livecheck, actual installation, signature/stapling/Gatekeeper verification, and exact-version packaged readiness |
|
||||
| Advisory disposition | Owner approved publication after supported artifacts were available. The [repository security advisory](https://github.com/BradGroux/veritas-kanban/security/advisories/GHSA-4r99-qpvh-wrqf) was published 2026-08-24 with the affected range and `>= 6.1.2` patched version verified |
|
||||
| Classified host limitation | The first local Docker wrapper and first Homebrew audit encountered the validation host's exhausted filesystem. No gate was weakened: clean CI proved the Docker contract, owner-approved removal of 1.67 GB of regenerable release output restored capacity, and the same live Homebrew audit/install then passed. No source, evidence, active development build, or user workspace was removed |
|
||||
|
||||
## 6.1.1 Publication Evidence
|
||||
|
||||
Source publication, signed-macOS verification, full-width release-note validation, isolated installed-app readiness, and Homebrew distribution are complete.
|
||||
|
||||
| Publication item | Result |
|
||||
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Release PR and merge | [#1157](https://github.com/BradGroux/veritas-kanban/pull/1157); reviewed head `5813d0d3d5285558582cb76c97e7d824b5ad9425`; verified squash merge `2cfb89396da7e115571f3c1449449ef60bde53d7` |
|
||||
| Annotated `v6.1.1` tag object and peeled commit | `c18955bbd7cc582f7ccd41a958ec1f53b6907cc2`; `2cfb89396da7e115571f3c1449449ef60bde53d7` |
|
||||
| GitHub release URL and body | https://github.com/BradGroux/veritas-kanban/releases/tag/v6.1.1; stable release published 2026-08-22; the live body exactly matches `docs/releases/v6.1.1.md` and is the latest non-prerelease |
|
||||
| Release CI and unsigned packaging | [CI run 32608829719](https://github.com/BradGroux/veritas-kanban/actions/runs/32608829719) passed build, security audit, lint/typecheck, scope selection, 3,771 workspace tests, and the changed-test gate; [artifact run 32608829776](https://github.com/BradGroux/veritas-kanban/actions/runs/32608829776) passed macOS, Linux, and Windows unsigned packaging on the reviewed head |
|
||||
| Desktop Release workflow URL and duration | [Run 32609140955](https://github.com/BradGroux/veritas-kanban/actions/runs/32609140955); exact release merge SHA; signed and notarized job passed in 11m37s |
|
||||
| Signed/notarized DMG | `Veritas-Kanban-6.1.1-mac-arm64.dmg`; 268,632,259 bytes; SHA-256 and published sidecar value `7fa44f4129be751757a7e049cd3681266cab3215402cd2e2a5d8b0acf38c6c11`; independent download, signature, Gatekeeper, and stapling validation pass |
|
||||
| Signed/notarized ZIP | `Veritas-Kanban-6.1.1-mac-arm64.zip`; 272,805,716 bytes; SHA-256 and published sidecar value `cd9c1cc68d474d3dbea8b6aae90380f5188f041009c229756371eeba7eecd9b0`; Homebrew strict online audit independently downloaded and accepted the asset |
|
||||
| DMG/ZIP blockmaps and SHA-256 sidecars | Blockmap digests `fe15ce89df44e720e3b1c9381b035d80a079042697a592c12b2be9cad96a949e` and `a5b9b910d458ab430daee0ae61ad962948c115bd69436fdbc4e56af861db38b8`; sidecar-file digests `7f29b0095ff92529b0c9fdcffac539fe4ae20adff772011f4ed14ca735a641c6` and `fd0cbb9aa58315f4143c1b4daf9aad158c150283086660a6be93829e42b9ccce` |
|
||||
| `latest-mac.yml` | Version 6.1.1; 530 bytes; SHA-256 `df7b4f16c3f6c21720f7446af531967854bc3901749f09aaa87e5d1d06b1eb11`; ZIP and DMG names, sizes, and SHA-512 values match the published assets |
|
||||
| Installed signed-app isolated launch | Homebrew upgraded the installed cask from 6.1.0 to 6.1.1. `CFBundleShortVersionString` reports 6.1.1; deep strict code-signature validation, Gatekeeper acceptance from notarized Developer ID `RLBHD62MPW`, and stapling validation pass. A disposable profile and workspace launched the packaged app on isolated port 3101; exact-version readiness passed in 4.289s and proved the packaged app owned the listener. `/api/health` reported 6.1.1, the process stopped cleanly, both disposable user-data directories moved to Trash, and the existing workspace was not opened. |
|
||||
| Release validator | Post-publication `pnpm validate:release -- --version 6.1.1 --skip-build-output --github --repo BradGroux/veritas-kanban` passes and proves the live release and body match the annotated tag and canonical checked-in file |
|
||||
| Homebrew cask | [Issue #48](https://github.com/BradGroux/homebrew-tap/issues/48); [PR #49](https://github.com/BradGroux/homebrew-tap/pull/49); reviewed head `4956cd0c7245ad28571f0860ce1f0d63159e4ff5`; verified merge `033983c2d76eccb6ef348e529a42f53d328ef335`; registered `bradgroux/tap/veritas-kanban` resolves 6.1.1 with the published ZIP checksum and passes Ruby syntax, cask style, strict online audit, dry-run install, livecheck, installed-cask upgrade, and exact-version packaged readiness |
|
||||
|
||||
## 6.1.0 Publication Evidence
|
||||
|
||||
Source publication, signed-macOS verification, full-width release-note validation, isolated installed-app readiness, and Homebrew distribution are complete.
|
||||
|
|
|
|||
|
|
@ -1,46 +1,50 @@
|
|||
# Veritas Kanban 6.1.1 Release Notes
|
||||
# Veritas Kanban 6.1.2 Release Notes
|
||||
|
||||
Veritas Kanban 6.1.1 is a focused maintenance release for the agentic-control platform delivered in 6.1.0. It restores reliable Task Detail drawer scrolling after the Mantine tabs migration, adds browser-level regression coverage, updates the supported dependency set, and refreshes transitive security floors.
|
||||
Veritas Kanban 6.1.2 completes the reliability, security, persistence, provider-runtime, CI, container, and supportability audit tracked by [#1174](https://github.com/BradGroux/veritas-kanban/issues/1174). It is a backward-compatible patch release for 6.1.1.
|
||||
|
||||
> Veritas Kanban 6.0.0 remains a quarantined prerelease. Version 6.1.1 supersedes 6.1.0 as the supported stable v6 release after signed assets and updater metadata are published.
|
||||
> Veritas Kanban 6.0.0 remains a quarantined prerelease. Version 6.1.2 is the supported stable v6 release; its annotated tag, signed assets, updater metadata, and Homebrew cask are published and verified.
|
||||
|
||||
## 6.1.1 Maintenance Changes
|
||||
## Audit Outcomes And Traceability
|
||||
|
||||
Long Task Detail content is height-constrained and scrollable again. The regression gate verifies the shared overlay's flex-column layout, real overflow, and wheel-driven scroll movement in Chromium. Nested task-card controls no longer activate the card, preserving touch status selection in WebKit after the Mantine 9.5 update. File-backed workflow operations now await storage-directory readiness, preventing immediate startup requests from racing directory creation. The dependency maintenance pass updates supported minor and patch versions, adopts Chalk 6, and refreshes transitive override floors so production and full dependency audits report no known vulnerabilities.
|
||||
| Issue | Operational outcome | Pull requests |
|
||||
| --- | --- | --- |
|
||||
| [#1162](https://github.com/BradGroux/veritas-kanban/issues/1162) | Canonical runtime data paths, legacy discovery, and migration compatibility | #1184 |
|
||||
| [#1163](https://github.com/BradGroux/veritas-kanban/issues/1163) | Service persistence restored behind explicit file and SQLite repositories | #1190-#1220 |
|
||||
| [#1164](https://github.com/BradGroux/veritas-kanban/issues/1164) | Provider launch, runtime, event, completion, mutation, and adapter contracts decomposed | #1223-#1230 |
|
||||
| [#1165](https://github.com/BradGroux/veritas-kanban/issues/1165) | Credential-aware JSON, blob, stream, and download API helpers | #1218 |
|
||||
| [#1166](https://github.com/BradGroux/veritas-kanban/issues/1166) | Measured non-root production Docker runtime and size contract | #1222 |
|
||||
| [#1167](https://github.com/BradGroux/veritas-kanban/issues/1167) | Immutable external GitHub Actions | #1179 |
|
||||
| [#1168](https://github.com/BradGroux/veritas-kanban/issues/1168) | Continuous CodeQL, dependency, and secret scanning | #1180 |
|
||||
| [#1169](https://github.com/BradGroux/veritas-kanban/issues/1169) | Risk-weighted critical-path coverage baselines and ratchets | #1183 |
|
||||
| [#1170](https://github.com/BradGroux/veritas-kanban/issues/1170) | Four unused direct dependencies removed | #1217 |
|
||||
| [#1171](https://github.com/BradGroux/veritas-kanban/issues/1171) | Native-loader-compatible Vite and Vitest configuration | #1178 |
|
||||
| [#1172](https://github.com/BradGroux/veritas-kanban/issues/1172) | Deterministic, milestone-scoped workspace and browser gates | #1175, #1177, #1181, #1228 |
|
||||
| [#1173](https://github.com/BradGroux/veritas-kanban/issues/1173) | Server lint-warning budget reduced from 600 to 458 | #1221 |
|
||||
| [#1231](https://github.com/BradGroux/veritas-kanban/issues/1231) | Initial CodeQL baseline triaged, remediated, and dispositioned | #1232-#1235 |
|
||||
|
||||
jsdom 30 remains deferred in Dependabot because it exceeds the documented Node.js patch floor and breaks the current changed-test suite; jsdom 29 patch updates remain enabled. Version 6.1.1 does not change the storage schema, public REST API, provider contracts, configuration, or migration requirements from 6.1.0.
|
||||
## Persistence And Runtime Paths
|
||||
|
||||
## Harness Support On Equal Footing
|
||||
`DATA_DIR` and `VERITAS_DATA_DIR` now resolve through one canonical path contract. Live services, health, backup, integrity, migrations, and the production container use the same root. Legacy locations remain discoverable and migrate through explicit compatibility paths rather than creating split authoritative state.
|
||||
|
||||
Every supported harness is now discovered, diagnosed, dispatched, observed, and completed through the same `harness-support-profile/v1`, `provider-runtime-manifest/v1`, immutable launch-manifest, approval, tool, credential, sandbox, phase-authority, and completion contracts. Settings, API diagnostics, telemetry, `vk doctor --json`, and dispatch consume the same redacted readiness evidence.
|
||||
Service-layer filesystem access has been moved into deep repository modules across activity, progress, status history, scheduled deliverables, workflows, broadcasts, conflicts, delegation, ceremony, error analyses, permissions, lifecycle configuration, scheduler, reflection, chat, tasks, telemetry, and managed content. File and SQLite backends preserve their containment, locking, atomic-write, and parity contracts.
|
||||
|
||||
Equal footing does not claim identical native capabilities. Veritas probes the exact installed build and transport, persists capability evidence, and blocks unsupported lifecycle, tool, approval, sandbox, network, phase, or completion behavior before attempt creation. Provider upgrades invalidate prior conformance evidence until the new build passes its deterministic fixtures.
|
||||
## Provider Runtime And Frontend API
|
||||
|
||||
Buzz Agent and Grok Build use ACP transports with exact initialize and capability evidence. Codex CLI, SDK, and app-server retain their distinct supervised lifecycles. Claude Code uses a strict bare-mode stream, GitHub Copilot CLI remains bounded to its public-preview ACP contract, Hermes retains one-shot execution, and OpenClaw retains explicit gateway policy. Buzz relay communication remains independent from execution authority: Buzz transports signed messages while Veritas owns tasks, attempts, tools, approvals, and completion.
|
||||
Provider work now flows through cohesive launch-compiler, runtime-resolution, event-interpreter, completion, attempt-lifecycle, and adapter-registry boundaries. Explicitly executable providers retain their supported behavior. Provider-less, unknown, or profile/adapter-mismatched records still fail before attempt creation and never route through an implicit OpenClaw fallback.
|
||||
|
||||
Repository-facing instructions are documented in [AGENTS.md](../AGENTS.md) and the reusable [agent template](AGENTS-TEMPLATE.md). Harness installation, authentication, capability limits, and remediation are documented in [Agent Providers](AGENT-PROVIDERS.md), with exact evidence in the [Harness Compatibility Matrix](HARNESS-COMPATIBILITY.md).
|
||||
Frontend JSON, blob, stream, log, and download operations now share credential-aware API boundaries. Cross-origin `VITE_API_URL` cookie authentication, configured base paths, and server error envelopes remain consistent across supported workflows.
|
||||
|
||||
## Governed Execution And Recovery
|
||||
## Verification, Security, Dependencies, And Container
|
||||
|
||||
Run-scoped egress enforcement resolves and pins allowed destinations, routes governed traffic through the gateway, applies protocol, host, port, HTTP method, and normalized path rules, and records redacted decision evidence. Required enforcement fails closed when a process can bypass or cannot prove the gateway.
|
||||
Ordinary pull requests now run source-policy, lint, typecheck, build, dependency-audit, secret-scanning, and CodeQL checks without repeatedly executing workspace tests, coverage, E2E, desktop packaging, load, or Docker contracts. Those expensive gates run at explicit `ci:full`, scheduled, manual, integration, security, and release milestones.
|
||||
|
||||
Durable admission control now applies capacity, aggregate budgets, fairness, cancellation, queue leases, and circuit breaking to direct tasks, workflows, retries, fallbacks, continuations, provider handoffs, and child agents through one execution-tree contract. Agent-dependency health feeds load shedding so an unhealthy tree cannot continue amplifying provider, host, or workspace pressure.
|
||||
The complete final release matrix is recorded in [v6 Release Candidate Evidence Packet](V6-RC-EVIDENCE-PACKET.md). Historical test counts are not reused as 6.1.2 evidence.
|
||||
|
||||
Append-only admission snapshots now complete each serialized write before syncing, preventing short filesystem writes from truncating durable reservation evidence. Knowledge-collection routes also share the exact server permission prefix, keeping client discovery and server enforcement in fail-closed parity.
|
||||
The production Docker closure excludes unrelated workspace dependencies, runs as a non-root user, and has architecture-specific size ceilings. The implementation baseline measured 195,910,880 bytes on arm64 against a 200,000,000-byte ceiling. The final release candidate measured 571,628,184 bytes on amd64 against its 600,000,000-byte ceiling.
|
||||
|
||||
Durable goals survive turns, restarts, and provider continuations without inventing completion. Memory extraction is reviewed and attributable. Background commands and monitors are supervisor-owned, repetitive or stalled runs receive bounded recovery, and oversized output spills into governed artifacts instead of exhausting the active context.
|
||||
Four verified unused direct dependencies were removed. The server lint-warning budget dropped from 600 to 458 without weakening rules or adding broad suppressions. A coordinated security remediation is integrated through #1236 and the [repository security advisory](https://github.com/BradGroux/veritas-kanban/security/advisories/GHSA-4r99-qpvh-wrqf) was published after the supported 6.1.2 artifacts were verified. Final milestone validation also corrected recovery-key alphabet generation, WebSocket upgrade header forwarding, same-task lifecycle ordering, and sanitized URI prefix handling through #1239, #1241, and #1243.
|
||||
|
||||
## Knowledge Collections And Integrity
|
||||
|
||||
Workspace knowledge collections support classified immutable sources, cited and versioned derived pages, reviewed ingestion dry runs, atomic apply and reversal, scoped keyword and QMD search, query promotion, and cited work-product export. File and SQLite backends preserve the same workspace, digest, attribution, idempotency, contradiction, graph, activity, and redaction contracts.
|
||||
|
||||
Deterministic integrity linting finds structural graph errors, invalid schemas and metadata, provenance gaps, source-hash drift, invalid citation locations, freshness violations, orphan pages, missing canonical terms, unanswered research questions, contradictions, near-duplicates, supersession candidates, and evidence gaps. Material claims have attributable, evidence-linked, reversible lifecycle controls, so disputed or superseded statements remain visible and reviewable rather than being silently overwritten.
|
||||
|
||||
## Workspace Checkpoints And Rewind
|
||||
|
||||
Turn-boundary checkpoints capture run-owned Git, index, file, exclusion, ownership, conversation, and attributable provider-diff state. Rewind is preview-first, digest-bound, conflict-aware, and limited to explicit selected paths. The control route can quiesce an exact active Codex app-server turn and fork an earlier approved turn into a new provider thread.
|
||||
|
||||
Ambiguous attribution, unsupported providers, stale runtime evidence, external edits, and unresolved ownership conflicts fail closed. Failed storage transactions preserve descendant state and do not mutate paths outside the approved preview.
|
||||
The initial 195-alert CodeQL baseline was reviewed alert by alert: 67 findings were fixed and 128 non-exploitable alerts received evidence-backed dispositions. Validated request, logging, persisted-key, file-handling, and sandbox-read findings were fixed in #1232-#1235. Alerts that were limited to test fixtures or were already contained by explicit authentication, path, descriptor, ownership, or atomic-write controls received documented dispositions rather than speculative code churn. The post-merge default-branch analysis reports zero open alerts.
|
||||
|
||||
## Install Or Upgrade
|
||||
|
||||
|
|
@ -57,31 +61,30 @@ For a first installation:
|
|||
brew install --cask bradgroux/tap/veritas-kanban
|
||||
```
|
||||
|
||||
Manual installation uses the signed and notarized macOS arm64 DMG or ZIP from the [v6.1.1 release](https://github.com/BradGroux/veritas-kanban/releases/tag/v6.1.1). Back up the complete stopped-writer workspace before upgrading and keep the backup until the new runtime is accepted.
|
||||
Manual installation uses the signed and notarized macOS arm64 DMG or ZIP from the [v6.1.2 release](https://github.com/BradGroux/veritas-kanban/releases/tag/v6.1.2). Back up the complete stopped-writer workspace before upgrading and keep the backup until the new runtime is accepted.
|
||||
|
||||
## Breaking Changes And Migration Warnings
|
||||
|
||||
Veritas Kanban 6.1.1 retains SQLite migrations 30 to 33 from 6.1.0. No new migration runs when upgrading from 6.1.0. Rollback to an older schema still requires restoring the stopped-writer pre-upgrade backup; do not open migrated data with an older binary.
|
||||
There is no public REST API version change, configuration breaking change, or new SQLite schema migration in 6.1.2. Migrations remain at 30 through 33. Runtime-path normalization can move legacy files into the configured canonical data directory; verify the selected data root, health, integrity, and backup evidence before resuming writers or automation.
|
||||
|
||||
The public REST API remains mounted at `v1`. Provider-less or adapter/profile-mismatched records do not fall through to OpenClaw. Unknown or changed provider builds lose certification until current probes and deterministic fixtures pass. Claude Code does not launch with `--dangerously-skip-permissions`. Credential-bound MCP servers remain available only through the mediated run-scoped bridge.
|
||||
Rollback is restore-first. Stop every writer. Reinstall 6.1.1 only when the current data contracts remain compatible; otherwise restore the complete pre-upgrade stopped-writer workspace. Never copy an older database over a live instance.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
Buzz Agent sessions remain in-memory and do not support session load/resume. Buzz files, reactions, forums, direct messages, and destructive edit/delete projection are not bridged. GitHub Copilot CLI ACP remains public preview. Grok Build's stable artifact still self-reports alpha and cannot be fully traced to the current public source tree. Claude Code's complete CLI implementation is not public, so certification is bound to exact release behavior and checked-in fixtures.
|
||||
Buzz Agent sessions remain in-memory and do not support session load/resume. Buzz files, reactions, forums, direct messages, and destructive edit/delete projection are not bridged. GitHub Copilot CLI ACP remains public preview. Grok Build's stable artifact still self-reports alpha and cannot be fully traced to the current public source tree. Claude Code's complete CLI implementation is not public, so certification remains bound to exact release behavior and checked-in fixtures.
|
||||
|
||||
Deterministic compatibility does not prove provider authentication, subscription availability, quota, or live inference. Linux and Windows desktop artifacts remain unsigned previews; signed and notarized macOS arm64 is the supported stable desktop distribution.
|
||||
|
||||
## Release Artifacts
|
||||
|
||||
The supported stable desktop release publishes signed and notarized `Veritas-Kanban-6.1.1-mac-arm64.dmg` and `Veritas-Kanban-6.1.1-mac-arm64.zip`, blockmaps, SHA-256 sidecars, and `latest-mac.yml` updater metadata from the annotated `v6.1.1` tag. Publication is complete only after GitHub assets, signature, Gatekeeper, stapling, updater, downloaded-app launch, and Homebrew installation have been verified.
|
||||
The supported stable desktop release provides signed and notarized `Veritas-Kanban-6.1.2-mac-arm64.dmg` and `Veritas-Kanban-6.1.2-mac-arm64.zip`, blockmaps, SHA-256 sidecars, and `latest-mac.yml` updater metadata from the annotated `v6.1.2` tag. Exact sizes, hashes, signing, notarization, stapling, Gatekeeper, launch, updater, workflow, release, and Homebrew evidence are recorded in [v6 Release Candidate Evidence Packet](V6-RC-EVIDENCE-PACKET.md).
|
||||
|
||||
## Documentation And Evidence
|
||||
|
||||
- [Agent guide and reusable template](AGENTS-TEMPLATE.md)
|
||||
- [Agent provider setup and operations](AGENT-PROVIDERS.md)
|
||||
- [Harness compatibility matrix](HARNESS-COMPATIBILITY.md)
|
||||
- [Buzz integration guide](BUZZ-INTEGRATION.md)
|
||||
- [v6 runtime architecture](architecture/V6-AGENT-RUNTIME-CONTROL-PLANE.md)
|
||||
- [v6 compatibility and release policy](V6-COMPATIBILITY-AND-RELEASE-POLICY.md)
|
||||
- [v6 upgrade and administration guide](V6-UPGRADE-INSTALL-ADMIN-GUIDE.md)
|
||||
- [v6 release candidate evidence](V6-RC-EVIDENCE-PACKET.md)
|
||||
- [Changelog](../CHANGELOG.md)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
# Veritas Kanban v6 Upgrade, Install, Remote, And Admin Guide
|
||||
|
||||
This is the release-facing operator guide for Veritas Kanban 6.1.1. The
|
||||
This is the release-facing operator guide for Veritas Kanban 6.1.2. The
|
||||
detailed provider commands live in [Agent Providers](AGENT-PROVIDERS.md), the
|
||||
machine-readable support contract is summarized in
|
||||
[Harness Compatibility](HARNESS-COMPATIBILITY.md), and Buzz relay setup lives
|
||||
in [Buzz Integration](BUZZ-INTEGRATION.md).
|
||||
|
||||
Documentation freshness: 2026-08-22 for Veritas Kanban 6.1.1.
|
||||
Documentation freshness: 2026-08-24 for Veritas Kanban 6.1.2.
|
||||
|
||||
Do not install 6.0.0. It is retained as a quarantined prerelease. Version 6.1.1
|
||||
is the current supported stable v6 build and supersedes 6.1.0.
|
||||
Do not install 6.0.0. It is retained as a quarantined prerelease. Version 6.1.2
|
||||
is the supported stable v6 release and supersedes 6.1.1.
|
||||
|
||||
## Fresh Mac Desktop Install
|
||||
|
||||
|
|
@ -21,8 +21,8 @@ brew install --cask veritas-kanban
|
|||
```
|
||||
|
||||
Manual installation uses
|
||||
`Veritas-Kanban-6.1.1-mac-arm64.zip` from the
|
||||
[v6.1.1 GitHub release](https://github.com/BradGroux/veritas-kanban/releases/tag/v6.1.1).
|
||||
`Veritas-Kanban-6.1.2-mac-arm64.zip` from the
|
||||
[v6.1.2 GitHub release](https://github.com/BradGroux/veritas-kanban/releases/tag/v6.1.2).
|
||||
Move `Veritas Kanban.app` into `/Applications`, launch it normally, and verify
|
||||
Settings -> Maintenance before enabling an agent or external integration.
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ For a new board:
|
|||
|
||||
1. Choose Board Only unless agent execution is required immediately.
|
||||
2. Create the local admin password and retain the recovery key securely.
|
||||
3. Confirm `/api/health` reports version 6.1.1.
|
||||
3. Confirm `/api/health` reports version 6.1.2.
|
||||
4. Create a governed backup before adding external credentials or relay
|
||||
mappings.
|
||||
|
||||
|
|
@ -58,14 +58,14 @@ equivalent v5.2.5 self-hosted workspace.
|
|||
preferred port are stopped before copying data.
|
||||
5. Preserve the complete workspace, not only the SQLite file. Keep the backup
|
||||
through release acceptance.
|
||||
6. Install v6.1.1 without replacing the workspace.
|
||||
6. Install v6.1.2 without replacing the workspace.
|
||||
7. Launch with the same profile. If setup appears for a populated database,
|
||||
choose **Use Existing Data**. Do not rerun file migration or restore over the
|
||||
populated database.
|
||||
8. Wait for the exact-version readiness gate:
|
||||
|
||||
```bash
|
||||
EXPECTED_VERSION=6.1.1
|
||||
EXPECTED_VERSION=6.1.2
|
||||
pnpm desktop:wait:ready -- --expected-version "$EXPECTED_VERSION"
|
||||
```
|
||||
|
||||
|
|
@ -82,8 +82,10 @@ The public API remains `v1`. v6 adds provider, approval, lifecycle, tool,
|
|||
credential, compatibility, Buzz, and conformance records without requiring a
|
||||
new API mount.
|
||||
|
||||
Veritas Kanban 6.1.1 retains the SQLite workspace migrations 30 to 33 from
|
||||
6.1.0. Keep the stopped-writer
|
||||
Veritas Kanban 6.1.2 retains the SQLite workspace migrations 30 to 33 from
|
||||
6.1.0. No new schema migration runs when upgrading from 6.1.1. Runtime-path
|
||||
normalization can move legacy files into the configured canonical data root.
|
||||
Keep the stopped-writer
|
||||
backup until collection, task, workflow, provider, and board data have been
|
||||
accepted. Rollback to an older schema requires restoring that backup; do not
|
||||
open migrated data with an older binary.
|
||||
|
|
|
|||
29
docs/architecture/SERVICE-FILESYSTEM-BOUNDARY.md
Normal file
29
docs/architecture/SERVICE-FILESYSTEM-BOUNDARY.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Service Filesystem Boundary
|
||||
|
||||
Service modules must use the storage abstraction instead of importing Node's
|
||||
filesystem APIs directly. The current exceptions are tracked in
|
||||
[`service-filesystem-boundary.json`](service-filesystem-boundary.json) so the
|
||||
existing migration debt is explicit without allowing it to spread.
|
||||
|
||||
Run the boundary gate with:
|
||||
|
||||
```bash
|
||||
pnpm check:service-filesystem-boundary
|
||||
```
|
||||
|
||||
The gate recursively scans `server/src/services/**/*.ts` and recognizes static
|
||||
imports, dynamic imports, and `require()` calls for `fs`, `node:fs`, and their
|
||||
`/promises` variants. Filesystem text embedded in strings and comments is
|
||||
ignored. The command exits nonzero and names the file when it finds:
|
||||
|
||||
- a direct import without a classified inventory entry;
|
||||
- an invalid category, owner, or rationale;
|
||||
- a duplicate entry; or
|
||||
- a stale entry after an import has been removed.
|
||||
|
||||
`maximumEntries` must equal the number of classified exceptions. Any increase
|
||||
therefore requires a visible inventory and ratchet change in the same review.
|
||||
The remaining #1163 child issues own the reductions: #1187 covers operational
|
||||
evidence, #1188 managed content, and #1189 final process I/O plus removal of the
|
||||
last compatibility exceptions. Each migration must delete its stale inventory
|
||||
entries and lower `maximumEntries` in the same change.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Veritas Kanban v6 Agent Runtime Control Plane
|
||||
|
||||
This document defines the supported v6.1.1 architecture for executable agent
|
||||
This document defines the supported v6.1.2 architecture for executable agent
|
||||
harnesses and Buzz integration. It is the version-level composition of the
|
||||
individual contract documents for
|
||||
[ACP](ACP-PROVIDER-V1.md),
|
||||
|
|
@ -9,7 +9,7 @@ individual contract documents for
|
|||
[tool control](TOOL-CONTROL-PLANE-V1.md), and
|
||||
[runtime hooks](RUNTIME-HOOK-V1.md).
|
||||
|
||||
Documentation freshness: 2026-08-22 for Veritas Kanban 6.1.1.
|
||||
Documentation freshness: 2026-08-24 for Veritas Kanban 6.1.2.
|
||||
|
||||
## Authority Model
|
||||
|
||||
|
|
@ -43,6 +43,12 @@ owns signed delivery, not Veritas task or completion state.
|
|||
Provider profiles select an adapter. No unknown executable, provider-less
|
||||
record, or unsupported profile can route through an implicit fallback.
|
||||
|
||||
The server resolves these contracts through a provider adapter registry. The
|
||||
registry owns the task-envelope renderer, runtime probe, event mapper, start
|
||||
dispatch, and stop semantics for each exact executable provider. Attempt state
|
||||
transitions remain centralized in the lifecycle coordinator, while terminal
|
||||
completion and recovery consume the same persisted provider evidence.
|
||||
|
||||
## Run Lifecycle
|
||||
|
||||
```text
|
||||
|
|
|
|||
5
docs/architecture/service-filesystem-boundary.json
Normal file
5
docs/architecture/service-filesystem-boundary.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"maximumEntries": 0,
|
||||
"entries": []
|
||||
}
|
||||
|
|
@ -32,15 +32,15 @@ This guide walks you through every self-hosting scenario — from running locall
|
|||
|
||||
| Requirement | Version | Install |
|
||||
| ----------- | ------- | ------------------------------------------------------------ |
|
||||
| Node.js | 22.0.0+ | https://nodejs.org or `nvm install 22` |
|
||||
| pnpm | 11.1.1+ | `corepack enable && corepack prepare pnpm@11.1.1 --activate` |
|
||||
| Git | any | https://git-scm.com |
|
||||
| Node.js | 22.22.1+ | https://nodejs.org or `nvm install 22` |
|
||||
| pnpm | 11.1.1 | `corepack enable && corepack prepare pnpm@11.1.1 --activate` |
|
||||
| Git | 2.38+ | https://git-scm.com |
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
node --version # v22.x.x
|
||||
pnpm --version # 11.x.x
|
||||
node --version # v22.22.1 or newer
|
||||
pnpm --version # 11.1.1
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -468,6 +468,10 @@ The `DATA_DIR=/app/data` volume holds all persistent data:
|
|||
└── logs/ # Application logs
|
||||
```
|
||||
|
||||
This tree is illustrative, not exhaustive. Veritas also stores workflows,
|
||||
runtime evidence, telemetry, provider records, and other governed domains under
|
||||
the same canonical root. Back up the entire stopped-writer volume.
|
||||
|
||||
**Without a named volume, data is lost on every `docker compose down`.** Always use a volume or bind mount.
|
||||
|
||||
For `VERITAS_STORAGE=sqlite`, persistence is not enough: the authoritative
|
||||
|
|
@ -614,8 +618,8 @@ Set `PROMETHEUS_METRICS_TOKEN` on the Veritas server to the same secret, or use
|
|||
|
||||
| Variable | Default | Description |
|
||||
| -------------------------- | -------------------- | ------------------------------------------------------- |
|
||||
| `VERITAS_DATA_DIR` | `.veritas-kanban` | Config, logs, internal state (relative to project root) |
|
||||
| `DATA_DIR` | `/app/data` (Docker) | Mapped data dir inside Docker container |
|
||||
| `VERITAS_DATA_DIR` | Project root | Storage root used when `DATA_DIR` is unset |
|
||||
| `DATA_DIR` | `/app/data` (Docker) | Preferred storage root; takes precedence |
|
||||
| `TELEMETRY_RETENTION_DAYS` | `30` | Days to keep telemetry event files |
|
||||
| `TELEMETRY_COMPRESS_DAYS` | `7` | Days after which telemetry files are gzip-compressed |
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ Don't use it when:
|
|||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js ≥ 22
|
||||
- Node.js ≥ 22.22.1
|
||||
- The Veritas Kanban server running (`pnpm dev` or production)
|
||||
- pnpm (for building from source)
|
||||
- No OpenClaw requirement unless OpenClaw is the MCP client or agent runner you choose
|
||||
|
|
@ -844,10 +844,10 @@ Configure telemetry retention in `server/.env`:
|
|||
|
||||
| Component | Version | Notes |
|
||||
| ------------------ | ------------ | --------------------------- |
|
||||
| MCP server package | `6.1.1` | Matches VK server version |
|
||||
| MCP server package | `6.1.2` | Matches VK server version |
|
||||
| MCP SDK | `1.29.0` | `@modelcontextprotocol/sdk` |
|
||||
| MCP protocol | `2025-11-25` | Latest stable spec |
|
||||
| Node.js | `≥ 22` | Matches the repo runtime |
|
||||
| Node.js | `≥ 22.22.1` | Matches the repo runtime |
|
||||
| TypeScript | `6.0+` | Build dependency only |
|
||||
|
||||
**Breaking change policy:**
|
||||
|
|
@ -888,4 +888,4 @@ The `findTask` utility matches the last N characters of a task ID (minimum 6). I
|
|||
|
||||
---
|
||||
|
||||
_Last updated: 2026-08-22 · VK v6.1.1 · 42 tools / 9 categories_
|
||||
_Last updated: 2026-08-24 · VK v6.1.2 · 42 tools / 9 categories_
|
||||
|
|
|
|||
61
docs/releases/v6.1.2.md
Normal file
61
docs/releases/v6.1.2.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
Veritas Kanban 6.1.2 completes the repository-wide reliability, security, storage, provider-runtime, CI, container, and supportability audit. It is a backward-compatible patch release for 6.1.1 with no public API or SQLite schema-version change.
|
||||
|
||||
## What changed
|
||||
|
||||
- **One runtime data root.** Paths, migration, backup, integrity, health, and Docker mounts use the canonical `DATA_DIR` contract.
|
||||
- **Storage boundary restored.** File and SQLite repositories own containment, locking, atomicity, and compatibility behavior.
|
||||
- **Explicit provider adapters.** Launch, probes, events, completion, mutation, and stop behavior fail closed without implicit fallback.
|
||||
- **One authenticated API client.** JSON, blob, stream, and download operations preserve credentials, base paths, and error envelopes.
|
||||
- **Milestone-scoped tests.** Ordinary pull requests stay fast; complete test and artifact gates run at declared integration and release milestones.
|
||||
- **Continuous supply-chain controls.** CodeQL, dependency alerts, secret scanning, immutable actions, and policy guards remain active.
|
||||
- **Resolved CodeQL baseline.** Reviewed all 195 initial alerts: fixed 67, dispositioned 128, and left zero open on the default branch.
|
||||
- **Measured production container.** The non-root runtime closure verifies health, auth, SQLite, static web, data paths, and size ceilings.
|
||||
- **Lower maintenance debt.** Four unused dependencies were removed and the server warning budget fell from 600 to 458.
|
||||
- **Coordinated security hardening.** The private remediation is integrated; disclosure remains gated on supported artifacts and approval.
|
||||
- **Release-gate corrections.** Runtime boundary behavior now matches its checked-in contracts.
|
||||
|
||||
## Issue and pull request traceability
|
||||
|
||||
| Audit issue | Outcome | Pull requests |
|
||||
| --- | --- | --- |
|
||||
| #1162 | Canonical runtime data paths and legacy compatibility | #1184 |
|
||||
| #1163 | Service/storage boundary restored across persisted domains | #1190-#1220 |
|
||||
| #1164 | Provider and attempt lifecycle decomposition | #1223-#1230 |
|
||||
| #1165 | Credential-aware frontend API migration | #1218 |
|
||||
| #1166 | Production Docker size and runtime contract | #1222 |
|
||||
| #1167 | Immutable GitHub Actions | #1179 |
|
||||
| #1168 | Continuous security scanning | #1180 |
|
||||
| #1169 | Critical-path coverage baselines and ratchets | #1183 |
|
||||
| #1170 | Unused dependency removal | #1217 |
|
||||
| #1171 | Native-loader-compatible Vite and Vitest configuration | #1178 |
|
||||
| #1172 | Deterministic, milestone-scoped test gates | #1175, #1177, #1181, #1228 |
|
||||
| #1173 | Server type-safety lint-debt ratchet | #1221 |
|
||||
| #1231 | Initial CodeQL alert baseline resolved | #1232-#1235 |
|
||||
| #1238 | Recovery-key alphabet and WebSocket header forwarding | #1239 |
|
||||
| #1240 | Same-task lifecycle invocation ordering | #1241 |
|
||||
| #1242 | Sanitized URI prefix validation | #1243 |
|
||||
|
||||
## Install or upgrade
|
||||
|
||||
Back up the complete stopped-writer workspace before upgrading and retain the backup until the new runtime is accepted.
|
||||
|
||||
```bash
|
||||
brew update
|
||||
brew upgrade --cask bradgroux/tap/veritas-kanban
|
||||
```
|
||||
|
||||
For a first installation:
|
||||
|
||||
```bash
|
||||
brew install --cask bradgroux/tap/veritas-kanban
|
||||
```
|
||||
|
||||
The Assets section provides signed and notarized macOS arm64 DMG and ZIP packages after the release workflow completes. Linux and Windows packages remain unsigned verification previews.
|
||||
|
||||
## Compatibility, migration, and rollback
|
||||
|
||||
The public REST API remains `v1`, and SQLite schema migrations remain at 30 through 33. Upgrading from 6.1.1 does not run a new schema migration. Runtime path normalization can migrate legacy files into the configured canonical data directory; operators should verify the data root, health, backup, and integrity evidence before resuming automation.
|
||||
|
||||
Rollback is restore-first. Stop every writer, reinstall 6.1.1 only when its data contracts remain compatible, and otherwise restore the complete stopped-writer pre-upgrade workspace. Never copy an older database over a running instance.
|
||||
|
||||
See the [full release notes](https://github.com/BradGroux/veritas-kanban/blob/v6.1.2/docs/V6-RELEASE-NOTES.md), [upgrade guide](https://github.com/BradGroux/veritas-kanban/blob/v6.1.2/docs/V6-UPGRADE-INSTALL-ADMIN-GUIDE.md), [compatibility policy](https://github.com/BradGroux/veritas-kanban/blob/v6.1.2/docs/V6-COMPATIBILITY-AND-RELEASE-POLICY.md), and [changelog](https://github.com/BradGroux/veritas-kanban/blob/v6.1.2/CHANGELOG.md).
|
||||
289
docs/testing/critical-path-coverage.json
Normal file
289
docs/testing/critical-path-coverage.json
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
{
|
||||
"schemaVersion": "critical-path-coverage/v1",
|
||||
"longTermTarget": {
|
||||
"lines": 80,
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"statements": 80
|
||||
},
|
||||
"packages": [
|
||||
{
|
||||
"id": "server",
|
||||
"report": "coverage/server/coverage-summary.json",
|
||||
"runner": {
|
||||
"maxWorkers": 1,
|
||||
"extraArgs": ["--hookTimeout=20000", "--testTimeout=30000"],
|
||||
"testFiles": [
|
||||
"src/__tests__/acp-stdio-provider.test.ts",
|
||||
"src/__tests__/admission-control-service.test.ts",
|
||||
"src/__tests__/agent-permission-repository.test.ts",
|
||||
"src/__tests__/broadcast-storage-service.test.ts",
|
||||
"src/__tests__/chat-repository.test.ts",
|
||||
"src/__tests__/chat-service.test.ts",
|
||||
"src/__tests__/task-file-repository.test.ts",
|
||||
"src/__tests__/claude-code-provider.smoke.test.ts",
|
||||
"src/__tests__/claude-code-provider.test.ts",
|
||||
"src/__tests__/codex-app-server-provider.smoke.test.ts",
|
||||
"src/__tests__/codex-app-server-provider.test.ts",
|
||||
"src/__tests__/codex-env.test.ts",
|
||||
"src/__tests__/codex-event-interpreter.test.ts",
|
||||
"src/__tests__/codex-provider-service.test.ts",
|
||||
"src/__tests__/ceremony-state-repository.test.ts",
|
||||
"src/__tests__/communication-adapter-schemas.test.ts",
|
||||
"src/__tests__/conflict-workspace-repository.test.ts",
|
||||
"src/__tests__/context-provider-health-service.test.ts",
|
||||
"src/__tests__/credential-broker-service.test.ts",
|
||||
"src/__tests__/delegation-repository.test.ts",
|
||||
"src/__tests__/enforcement.test.ts",
|
||||
"src/__tests__/error-analysis-repository.test.ts",
|
||||
"src/__tests__/filesystem-sandbox-service.test.ts",
|
||||
"src/__tests__/harness-support-profile-schemas.test.ts",
|
||||
"src/__tests__/hermes-provider.test.ts",
|
||||
"src/__tests__/lifecycle-hooks-repository.test.ts",
|
||||
"src/__tests__/log-redaction.test.ts",
|
||||
"src/__tests__/middleware/auth.test.ts",
|
||||
"src/__tests__/middleware/write-enforcement.integration.test.ts",
|
||||
"src/__tests__/openclaw-provider.test.ts",
|
||||
"src/__tests__/path-audit.test.ts",
|
||||
"src/__tests__/progress-service.test.ts",
|
||||
"src/__tests__/provider-completion-service.test.ts",
|
||||
"src/__tests__/provider-launch-credential-plan-service.test.ts",
|
||||
"src/__tests__/provider-runtime-adapters.test.ts",
|
||||
"src/__tests__/provider-runtime-capability-service.test.ts",
|
||||
"src/__tests__/provider-runtime-control-service.test.ts",
|
||||
"src/__tests__/provider-runtime-manifest-service.test.ts",
|
||||
"src/__tests__/provider-task-envelope-renderer.test.ts",
|
||||
"src/__tests__/reflection-extraction-job-service.test.ts",
|
||||
"src/__tests__/reflection-state-repository.test.ts",
|
||||
"src/__tests__/runtime-paths.test.ts",
|
||||
"src/__tests__/scheduler-state-repository.test.ts",
|
||||
"src/__tests__/scheduled-deliverables-repository.test.ts",
|
||||
"src/__tests__/routes/admin-governance-auth.test.ts",
|
||||
"src/__tests__/routes/auth.test.ts",
|
||||
"src/__tests__/routes/credential-broker.test.ts",
|
||||
"src/__tests__/routes/sqlite-journal-maintenance.test.ts",
|
||||
"src/__tests__/routes/v1-permission-guards.test.ts",
|
||||
"src/__tests__/run-launch-manifest-service.test.ts",
|
||||
"src/__tests__/run-recovery-policy-service.test.ts",
|
||||
"src/__tests__/run-supervisor-service.test.ts",
|
||||
"src/__tests__/schemas.test.ts",
|
||||
"src/__tests__/security-legacy-runtime.test.ts",
|
||||
"src/__tests__/shared-api-permissions.test.ts",
|
||||
"src/__tests__/status-history-repository.test.ts",
|
||||
"src/__tests__/sqlite-journal-ownership-policy.test.ts",
|
||||
"src/__tests__/sqlite-maintenance-bootstrap.test.ts",
|
||||
"src/__tests__/sqlite-portability-service.test.ts",
|
||||
"src/__tests__/system-health-service.test.ts",
|
||||
"src/__tests__/storage/dual-storage-parity.test.ts",
|
||||
"src/__tests__/storage/file-storage.test.ts",
|
||||
"src/__tests__/storage/sqlite-audit-policy-repositories.test.ts",
|
||||
"src/__tests__/storage/sqlite-chat-repositories.test.ts",
|
||||
"src/__tests__/storage/sqlite-config-repositories.test.ts",
|
||||
"src/__tests__/storage/sqlite-dashboard-metrics.test.ts",
|
||||
"src/__tests__/storage/sqlite-database.test.ts",
|
||||
"src/__tests__/storage/sqlite-governance-repositories.test.ts",
|
||||
"src/__tests__/storage/sqlite-identity-repository.test.ts",
|
||||
"src/__tests__/storage/sqlite-notification-repositories.test.ts",
|
||||
"src/__tests__/storage/sqlite-operational-repositories.test.ts",
|
||||
"src/__tests__/storage/sqlite-performance.test.ts",
|
||||
"src/__tests__/storage/sqlite-provenance-repository.test.ts",
|
||||
"src/__tests__/storage/sqlite-scheduled-deliverables-repository.test.ts",
|
||||
"src/__tests__/storage/sqlite-storage.test.ts",
|
||||
"src/__tests__/storage/sqlite-task-repository.test.ts",
|
||||
"src/__tests__/storage/sqlite-work-products.test.ts",
|
||||
"src/__tests__/storage/sqlite-workflow-repositories.test.ts",
|
||||
"src/__tests__/storage/sqlite-workflow-run-execution.test.ts",
|
||||
"src/__tests__/task-service-sqlite.test.ts",
|
||||
"src/__tests__/transition-hooks-config-repository.test.ts",
|
||||
"src/__tests__/work-product-run-launch-manifest.test.ts",
|
||||
"src/__tests__/work-product-schemas.test.ts",
|
||||
"src/__tests__/workflow-definition-repository.test.ts",
|
||||
"src/__tests__/workflow-execution-file-repository.test.ts",
|
||||
"src/__tests__/workflow-run-repository.test.ts",
|
||||
"src/__tests__/workflow-run-service.test.ts",
|
||||
"src/__tests__/workflow-service.test.ts",
|
||||
"src/__tests__/workflow-step-executor-codex.test.ts",
|
||||
"src/__tests__/workflow-step-executor-openclaw.test.ts",
|
||||
"src/storage/sqlite/database.test.ts",
|
||||
"src/storage/sqlite/filesystem-posture.test.ts"
|
||||
]
|
||||
},
|
||||
"boundaries": [
|
||||
{
|
||||
"id": "dispatch-runtime",
|
||||
"description": "Provider dispatch, persisted runtime evidence, workflow execution, completion, and recovery.",
|
||||
"include": [
|
||||
"server/src/services/clawdbot-agent-service.ts",
|
||||
"server/src/services/codex-event-interpreter.ts",
|
||||
"server/src/services/*provider*.ts",
|
||||
"server/src/services/run-launch-compiler.ts",
|
||||
"server/src/services/workflow-run-service.ts",
|
||||
"server/src/services/workflow-step-executor.ts",
|
||||
"server/src/services/run-launch-manifest-service.ts",
|
||||
"server/src/services/run-recovery-policy-service.ts",
|
||||
"server/src/services/run-supervisor-service.ts"
|
||||
],
|
||||
"thresholds": {
|
||||
"lines": 64.97,
|
||||
"branches": 57.48,
|
||||
"functions": 70.22,
|
||||
"statements": 63.67
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "auth-validation-redaction",
|
||||
"description": "Authentication, authorization, validation, path containment, environment allowlists, and redaction.",
|
||||
"include": [
|
||||
"server/src/middleware/auth.ts",
|
||||
"server/src/middleware/local-agent-capability.ts",
|
||||
"server/src/middleware/validate.ts",
|
||||
"server/src/middleware/workflow-auth.ts",
|
||||
"server/src/config/security.ts",
|
||||
"server/src/lib/redact.ts",
|
||||
"server/src/utils/codex-env.ts",
|
||||
"server/src/utils/hermes-env.ts",
|
||||
"server/src/utils/paths.ts",
|
||||
"server/src/utils/sanitize.ts",
|
||||
"server/src/schemas/**/*.ts",
|
||||
"shared/src/utils/api-permissions.ts"
|
||||
],
|
||||
"thresholds": {
|
||||
"lines": 57.58,
|
||||
"branches": 52.85,
|
||||
"functions": 50.91,
|
||||
"statements": 56.89
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storage-locks-migrations",
|
||||
"description": "File and SQLite repositories, locks, migrations, and storage containment.",
|
||||
"include": ["server/src/storage/**/*.ts"],
|
||||
"thresholds": {
|
||||
"lines": 39.51,
|
||||
"branches": 34.11,
|
||||
"functions": 46.9,
|
||||
"statements": 38.73
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "web",
|
||||
"report": "coverage/web/coverage-summary.json",
|
||||
"runner": {
|
||||
"extraArgs": ["--testTimeout=15000"],
|
||||
"testFiles": [
|
||||
"src/__tests__/api-helpers.test.ts",
|
||||
"src/__tests__/api-no-raw-fetch.test.ts",
|
||||
"src/__tests__/api-tasks.test.ts",
|
||||
"src/__tests__/archive-backlog-mantine.test.tsx",
|
||||
"src/__tests__/auth-guard.test.tsx",
|
||||
"src/__tests__/auth-screens-mantine.test.tsx",
|
||||
"src/__tests__/use-task-sync-admission.test.ts",
|
||||
"src/__tests__/useTasks-patchCache.test.ts",
|
||||
"src/__tests__/useRealtimeAgentStatus.test.ts",
|
||||
"src/__tests__/useWebSocket.test.ts"
|
||||
]
|
||||
},
|
||||
"boundaries": [
|
||||
{
|
||||
"id": "api-auth-realtime",
|
||||
"description": "API envelopes, authentication/session behavior, critical task mutations, and realtime reconciliation.",
|
||||
"include": [
|
||||
"web/src/lib/api/**/*.ts",
|
||||
"web/src/hooks/useAuth.tsx",
|
||||
"web/src/hooks/useIdentity.tsx",
|
||||
"web/src/hooks/useWebSocket.ts",
|
||||
"web/src/hooks/useTaskSync.ts",
|
||||
"web/src/hooks/useTasks.ts",
|
||||
"web/src/hooks/useBacklog.ts",
|
||||
"web/src/contexts/WebSocketContext.tsx",
|
||||
"web/src/lib/client-policy.ts",
|
||||
"web/src/lib/sanitize.ts"
|
||||
],
|
||||
"thresholds": {
|
||||
"lines": 37.74,
|
||||
"branches": 34.36,
|
||||
"functions": 28.67,
|
||||
"statements": 36.79
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cli",
|
||||
"report": "coverage/cli/coverage-summary.json",
|
||||
"runner": {
|
||||
"triggerPatterns": ["cli/src/**/*.test.ts"]
|
||||
},
|
||||
"boundaries": [
|
||||
{
|
||||
"id": "api-compatibility",
|
||||
"description": "CLI API envelopes and critical agent, admission, task, SQLite, and diagnostic commands.",
|
||||
"include": [
|
||||
"cli/src/utils/api.ts",
|
||||
"cli/src/commands/agents.ts",
|
||||
"cli/src/commands/admission.ts",
|
||||
"cli/src/commands/tasks.ts",
|
||||
"cli/src/commands/sqlite.ts",
|
||||
"cli/src/commands/doctor.ts"
|
||||
],
|
||||
"thresholds": {
|
||||
"lines": 52.07,
|
||||
"branches": 44.53,
|
||||
"functions": 65.45,
|
||||
"statements": 51.02
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mcp",
|
||||
"report": "coverage/mcp/coverage-summary.json",
|
||||
"runner": {
|
||||
"triggerPatterns": ["mcp/src/**/*.test.ts"]
|
||||
},
|
||||
"boundaries": [
|
||||
{
|
||||
"id": "api-tool-contracts",
|
||||
"description": "MCP API failure envelopes and task, agent, project, sprint, and control-plane tools.",
|
||||
"include": ["mcp/src/utils/api.ts", "mcp/src/tools/**/*.ts"],
|
||||
"thresholds": {
|
||||
"lines": 52.39,
|
||||
"branches": 43.67,
|
||||
"functions": 60,
|
||||
"statements": 51.44
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "desktop",
|
||||
"report": "coverage/desktop/coverage-summary.json",
|
||||
"runner": {
|
||||
"triggerPatterns": ["desktop/src/**/*.test.ts"]
|
||||
},
|
||||
"boundaries": [
|
||||
{
|
||||
"id": "preload-ipc-trust",
|
||||
"description": "Preload exposure, IPC bridge contracts, navigation, secrets, updates, and process supervision.",
|
||||
"include": [
|
||||
"desktop/src/preload/index.ts",
|
||||
"desktop/src/shared/desktop-bridge-contracts.ts",
|
||||
"desktop/src/main/bridge.ts",
|
||||
"desktop/src/main/navigation.ts",
|
||||
"desktop/src/main/secrets.ts",
|
||||
"desktop/src/main/updates.ts",
|
||||
"desktop/src/main/process-supervisor.ts"
|
||||
],
|
||||
"thresholds": {
|
||||
"lines": 66.24,
|
||||
"branches": 66.54,
|
||||
"functions": 54.86,
|
||||
"statements": 65.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
101
docs/testing/critical-path-coverage.md
Normal file
101
docs/testing/critical-path-coverage.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Critical-path coverage ratchets
|
||||
|
||||
Veritas measures risk-weighted execution boundaries instead of presenting one repository-wide
|
||||
percentage as a quality claim. The governed policy is
|
||||
[`critical-path-coverage.json`](critical-path-coverage.json).
|
||||
|
||||
## Run the coverage gate
|
||||
|
||||
```bash
|
||||
pnpm test:coverage
|
||||
```
|
||||
|
||||
The command builds `@veritas-kanban/shared`, then runs each applicable workspace from its own
|
||||
working directory. It uses one Vitest worker for the lock-sensitive server boundary suite and at
|
||||
most four elsewhere. This preserves package-specific path and timeout behavior while collecting
|
||||
V8 coverage for `server`, `web`, `cli`, `mcp`, and `desktop`. Server and web execute the governed
|
||||
critical-path test files listed exactly in the policy; the smaller CLI, MCP, and desktop suites run completely. Live provider and MCP integration opt-ins are removed from the coverage process environment. The ordinary
|
||||
canonical unit gate remains responsible for every test. Coverage requires no live credentials or
|
||||
external services.
|
||||
|
||||
To measure only the packages selected by CI:
|
||||
|
||||
```bash
|
||||
pnpm test:coverage --packages server,web
|
||||
```
|
||||
|
||||
Each measured package writes HTML, `coverage-summary.json`, and statement-level
|
||||
`coverage-final.json` under `coverage/<package>/`.
|
||||
The root `coverage/critical-path-summary.json` and `.md` files contain the machine-readable and
|
||||
human-readable boundary results. Coverage output is generated evidence and remains ignored by
|
||||
Git.
|
||||
|
||||
## Governed boundaries
|
||||
|
||||
The initial floors were measured on 2026-08-23 with Node 22-compatible Vitest 4.1.11 and V8. A
|
||||
floor is the exact measured percentage, not a rounded repository target. Any lower line, branch,
|
||||
function, or statement result fails the gate. CI checks out complete Git history and compares the
|
||||
policy with the event's base commit, so removing boundaries or runner inputs, redirecting a
|
||||
report, narrowing include patterns, or lowering a floor fails even when the edited policy would
|
||||
otherwise pass.
|
||||
|
||||
| Package | Boundary | Lines | Branches | Functions | Statements |
|
||||
| ------- | ------------------------- | -----: | -------: | --------: | ---------: |
|
||||
| server | dispatch-runtime | 64.97% | 57.48% | 70.22% | 63.67% |
|
||||
| server | auth-validation-redaction | 57.58% | 52.85% | 50.91% | 56.89% |
|
||||
| server | storage-locks-migrations | 39.51% | 34.11% | 46.90% | 38.73% |
|
||||
| web | api-auth-realtime | 37.74% | 34.36% | 28.67% | 36.79% |
|
||||
| cli | api-compatibility | 52.07% | 44.53% | 65.45% | 51.02% |
|
||||
| mcp | api-tool-contracts | 52.39% | 43.67% | 60.00% | 51.44% |
|
||||
| desktop | preload-ipc-trust | 66.24% | 66.54% | 54.86% | 65.50% |
|
||||
|
||||
The long-term floor for every critical boundary and metric is 80%. Raise ratchets whenever added
|
||||
tests improve a result. Prioritize branches and functions below 80%, starting with the web API,
|
||||
authentication, mutation, and realtime boundary. Do not reduce a floor to make CI pass.
|
||||
|
||||
## CI behavior
|
||||
|
||||
The deterministic scope selector emits `coverage_packages` separately from ordinary affected
|
||||
workspaces. A full verification run measures all five packages. A focused run measures only a
|
||||
package whose governed critical-path source or tests changed. CI publishes the complete `coverage/`
|
||||
directory for 14 days and writes the boundary table to the job summary. Documentation and
|
||||
non-critical source changes do not repeat the coverage suite. Artifact upload uses `always()` so
|
||||
partial and failing reports remain available for diagnosis.
|
||||
|
||||
Broad source patterns in the policy automatically include new files in governed areas. Because
|
||||
workspace coverage uses `all: true`, an untested critical file contributes zero coverage and
|
||||
drops its boundary. CI additionally rejects every changed governed source file with no executable
|
||||
coverage entry or zero covered lines, so stronger coverage elsewhere cannot hide new untested code.
|
||||
Every added or modified executable statement in governed source must be covered by the measured
|
||||
suite or carry an explicit reviewed exception. The gate reads the base-to-head diff alongside the
|
||||
statement-level V8 report, so historical coverage elsewhere in an existing file cannot satisfy the
|
||||
new-code test requirement. Comments and type-only edits do not manufacture executable statements.
|
||||
Ambiguous mixed rewrites fail closed: if an unequal-line hunk combines runtime-neutral formatting
|
||||
with a runtime change and cannot be separated safely, all changed lines in that hunk require
|
||||
coverage. This favors enforcement over guessing and bounds the classification work per hunk.
|
||||
The authentication boundary includes server Zod schemas and the shared authoritative API
|
||||
permission map in addition to middleware and redaction code.
|
||||
|
||||
## Exclusions and reviewed exceptions
|
||||
|
||||
Generated output, test files, fixture directories, declaration files, type-only `types.ts` modules,
|
||||
and the SQLite test helper are excluded consistently in workspace Vitest configs. Production
|
||||
runtime source is not globally excluded.
|
||||
|
||||
If a critical source file cannot be tested immediately, add a narrow `exceptions` entry to its
|
||||
boundary with all four fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "server/src/example.ts",
|
||||
"reason": "Why automated coverage is not currently practical.",
|
||||
"owner": "BradGroux",
|
||||
"trackingIssue": "#1234",
|
||||
"reviewBy": "2026-09-30"
|
||||
}
|
||||
```
|
||||
|
||||
Exception paths must name one exact repository-relative file; globs and traversal are rejected.
|
||||
The reason must be substantive, the owner must be a GitHub login, the tracking issue must remain
|
||||
linked, and the real ISO review date must fall within 90 days. The pull request must explain the
|
||||
compensating verification and receive normal review. Remove the exception when coverage lands.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Lint Warning Debt
|
||||
|
||||
Review date: 2026-06-04
|
||||
Review date: 2026-08-24
|
||||
|
||||
The repository still allows lint warnings, but warning debt is now managed with a
|
||||
ratchetable budget and a repeatable package/rule report.
|
||||
|
|
@ -30,24 +30,15 @@ pnpm lint:report
|
|||
|
||||
## Current Budget
|
||||
|
||||
Current warning budget: 600.
|
||||
Current warning budget: 458.
|
||||
|
||||
Baseline after the production unused-value cleanup:
|
||||
The 6.1.2 audit reduced the repository ceiling from 600 to 458 by narrowing
|
||||
production server boundaries, replacing unsafe assertions, and removing unused
|
||||
values without relaxing rules or adding broad suppressions. Use
|
||||
`pnpm lint:report` for the current package and rule distribution; do not copy a
|
||||
historical distribution into release evidence.
|
||||
|
||||
| Package | Warnings |
|
||||
| ------- | -------- |
|
||||
| server | 536 |
|
||||
| web | 37 |
|
||||
| mcp | 25 |
|
||||
| shared | 2 |
|
||||
|
||||
Current warning classes:
|
||||
|
||||
| Rule | Warnings |
|
||||
| ------------------------------------------ | -------- |
|
||||
| `@typescript-eslint/no-explicit-any` | 342 |
|
||||
| `@typescript-eslint/no-non-null-assertion` | 227 |
|
||||
| `@typescript-eslint/no-unused-vars` | 31 |
|
||||
The final 6.1.2 release matrix records the freshly measured total and report.
|
||||
|
||||
## Cleanup Order
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
# Scheduled QA Gates
|
||||
|
||||
Review date: 2026-06-04
|
||||
Review date: 2026-08-24
|
||||
|
||||
The scheduled QA workflow runs heavier browser and load-test coverage outside
|
||||
the fast pull-request path. Pull requests stay limited to lint, typecheck,
|
||||
workspace unit tests, build, production dependency audit, and the desktop
|
||||
artifact gate when relevant.
|
||||
the fast pull-request path. Ordinary pull requests stay limited to lint,
|
||||
typecheck, build, production dependency audit, security gates, and test-scope
|
||||
recording. Workspace tests, coverage, desktop artifacts, and Docker contracts
|
||||
run only at scheduled, manual, or `ci:full` milestones.
|
||||
|
||||
The 2026-06-04 audit found the workflow failing before job creation because
|
||||
job-level `env` used the `runner.temp` context. GitHub does not expose the
|
||||
|
|
@ -13,12 +14,10 @@ job-level `env` used the `runner.temp` context. GitHub does not expose the
|
|||
error instead of producing logs. The workflow now writes `VERITAS_DATA_DIR`
|
||||
from `$RUNNER_TEMP` during job setup.
|
||||
|
||||
Playwright and `pnpm qa:mantine` remain scheduled/manual gates while #568 and
|
||||
#569 are open. Adding them to PR CI before those gates are stable would create
|
||||
red PR checks with known non-PR-specific failures. Once both gates pass on
|
||||
`main`, either add a small PR smoke job for `pnpm qa:mantine` and
|
||||
`pnpm test:e2e -- e2e/mantine-qa-gate.spec.ts`, or record the release decision
|
||||
to keep them scheduled-only here.
|
||||
Playwright, Mantine QA, coverage, desktop artifacts, load profiles, and Docker
|
||||
contracts are deliberate milestone gates. Apply `ci:full` to an integration or
|
||||
release candidate, or use the documented scheduled/manual dispatch. Do not add
|
||||
them to every ordinary pull request merely to duplicate the final milestone.
|
||||
|
||||
## Workflow
|
||||
|
||||
|
|
@ -30,6 +29,7 @@ Workflow file:
|
|||
|
||||
Triggers:
|
||||
|
||||
- Pull request carrying `ci:full`, rerun on each synchronized candidate head.
|
||||
- Weekly schedule: Monday at 08:17 UTC.
|
||||
- Manual dispatch: `workflow_dispatch`.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@veritas-kanban/mcp",
|
||||
"version": "6.1.1",
|
||||
"version": "6.1.2",
|
||||
"description": "MCP server for Veritas Kanban",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
@ -10,12 +10,12 @@
|
|||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsx src/index.ts",
|
||||
"test": "vitest run --maxWorkers=4",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"@veritas-kanban/shared": "workspace:*",
|
||||
"hono": "^4.13.2",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -6,5 +6,21 @@ export default defineConfig({
|
|||
exclude: ['**/node_modules/**', '**/dist/**'],
|
||||
globals: true,
|
||||
testTimeout: 15000, // Integration tests hit the live server
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: [
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.d.ts',
|
||||
'src/__tests__/**',
|
||||
'src/**/__fixtures__/**',
|
||||
'src/**/fixtures/**',
|
||||
'src/**/generated/**',
|
||||
'src/**/*.generated.*',
|
||||
'src/**/types.ts',
|
||||
'src/types/**/*.ts',
|
||||
],
|
||||
all: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
22
package.json
22
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "veritas-kanban",
|
||||
"version": "6.1.1",
|
||||
"version": "6.1.2",
|
||||
"private": true,
|
||||
"description": "Local-first task management and AI agent orchestration platform",
|
||||
"author": "Brad Groux <brad@digitalmeld.io>",
|
||||
|
|
@ -31,16 +31,25 @@
|
|||
"dev:watchdog": "bash scripts/dev-watchdog.sh",
|
||||
"build": "pnpm --filter @veritas-kanban/shared build && pnpm --filter @veritas-kanban/server build && pnpm --filter @veritas-kanban/web build && pnpm --filter @veritas-kanban/cli build && pnpm --filter @veritas-kanban/mcp build && pnpm --filter @veritas-kanban/desktop build",
|
||||
"lint": "eslint .",
|
||||
"lint:budget": "node scripts/lint-warning-budget.mjs --max-warnings=600",
|
||||
"lint:budget": "node scripts/lint-warning-budget.mjs --max-warnings=458",
|
||||
"lint:report": "node scripts/lint-warning-budget.mjs --all-rules",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"check:delivery-cadence": "node --test scripts/check-delivery-cadence.test.mjs && node scripts/check-delivery-cadence.mjs",
|
||||
"check:actions-pinned": "node --test scripts/check-actions-pinned.test.mjs && node scripts/check-actions-pinned.mjs",
|
||||
"check:security-gates": "node --test scripts/check-security-gates.test.mjs && node scripts/check-security-gates.mjs",
|
||||
"check:gitleaks": "node scripts/check-gitleaks.mjs",
|
||||
"check:vite-native-config": "pnpm --filter @veritas-kanban/web exec vite build --configLoader native && vitest run --configLoader native web/src/lib/__tests__/client-policy.test.ts",
|
||||
"check:pnpm-settings": "node scripts/check-pnpm-settings.mjs",
|
||||
"check:security-artifacts": "node scripts/check-security-artifacts.mjs",
|
||||
"check:tracked-ignore": "node --test scripts/check-tracked-ignore.test.mjs && node scripts/check-tracked-ignore.mjs",
|
||||
"check:service-filesystem-boundary": "node --test scripts/check-service-filesystem-boundary.test.mjs && node scripts/check-service-filesystem-boundary.mjs",
|
||||
"check:docker-image": "node scripts/check-docker-image.mjs",
|
||||
"typecheck": "pnpm --filter @veritas-kanban/shared build && pnpm -r typecheck",
|
||||
"test": "vitest run",
|
||||
"test:unit": "pnpm -r --workspace-concurrency=1 test",
|
||||
"test": "pnpm test:unit",
|
||||
"test:unit": "node --test scripts/run-workspace-unit-tests.test.mjs && node scripts/run-workspace-unit-tests.mjs",
|
||||
"test:ci-scope": "node --test scripts/select-ci-test-scope.test.mjs",
|
||||
"test:coverage": "node scripts/run-coverage.mjs",
|
||||
"check:coverage-policy": "node --test scripts/check-coverage-policy.test.mjs scripts/check-coverage-ratchets.test.mjs scripts/run-coverage.test.mjs && node scripts/check-coverage-policy.mjs",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
|
|
@ -67,7 +76,8 @@
|
|||
"@types/node": "^26.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.67.0",
|
||||
"@typescript-eslint/parser": "^8.67.0",
|
||||
"concurrently": "^10.0.4",
|
||||
"@vitest/coverage-v8": "^4.1.11",
|
||||
"concurrently": "^10.0.5",
|
||||
"eslint": "10.8.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
|
|
@ -75,7 +85,7 @@
|
|||
"lint-staged": "^17.3.0",
|
||||
"prettier": "^3.9.6",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.10"
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default defineConfig({
|
|||
testDir: './e2e',
|
||||
fullyParallel: false, // Run sequentially — tests may share board state
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
retries: 0,
|
||||
workers: 1,
|
||||
reporter:
|
||||
process.env.CI && process.env.PLAYWRIGHT_HTML_REPORT === '1'
|
||||
|
|
@ -45,7 +45,7 @@ export default defineConfig({
|
|||
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:3000',
|
||||
trace: 'on-first-retry',
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
// Auth — read admin key from server/.env (loaded via dotenv above)
|
||||
extraHTTPHeaders: {
|
||||
|
|
|
|||
433
pnpm-lock.yaml
generated
433
pnpm-lock.yaml
generated
|
|
@ -47,9 +47,12 @@ importers:
|
|||
'@typescript-eslint/parser':
|
||||
specifier: ^8.67.0
|
||||
version: 8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(vitest@4.1.11)
|
||||
concurrently:
|
||||
specifier: ^10.0.4
|
||||
version: 10.0.4
|
||||
specifier: ^10.0.5
|
||||
version: 10.0.5
|
||||
eslint:
|
||||
specifier: 10.8.1
|
||||
version: 10.8.1(jiti@2.7.0)
|
||||
|
|
@ -72,8 +75,8 @@ importers:
|
|||
specifier: ^6.0.3
|
||||
version: 6.0.3
|
||||
vitest:
|
||||
specifier: ^4.1.10
|
||||
version: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(@types/node@26.2.0)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
|
||||
cli:
|
||||
dependencies:
|
||||
|
|
@ -110,8 +113,8 @@ importers:
|
|||
specifier: ^26.2.0
|
||||
version: 26.2.0
|
||||
electron:
|
||||
specifier: ^43.4.0
|
||||
version: 43.4.0
|
||||
specifier: ^43.4.1
|
||||
version: 43.4.1
|
||||
electron-builder:
|
||||
specifier: ^26.15.3
|
||||
version: 26.15.3(electron-builder-squirrel-windows@26.15.3)
|
||||
|
|
@ -125,8 +128,8 @@ importers:
|
|||
specifier: ^8.2.1
|
||||
version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0)
|
||||
vitest:
|
||||
specifier: ^4.1.10
|
||||
version: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(@types/node@26.2.0)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
|
||||
mcp:
|
||||
dependencies:
|
||||
|
|
@ -136,9 +139,6 @@ importers:
|
|||
'@veritas-kanban/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
hono:
|
||||
specifier: '>=4.12.34'
|
||||
version: 4.13.2
|
||||
zod:
|
||||
specifier: ^4.4.3
|
||||
version: 4.4.3
|
||||
|
|
@ -156,8 +156,8 @@ importers:
|
|||
server:
|
||||
dependencies:
|
||||
'@openai/codex-sdk':
|
||||
specifier: 0.147.0
|
||||
version: 0.147.0
|
||||
specifier: 0.148.0
|
||||
version: 0.148.0
|
||||
'@veritas-kanban/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
|
|
@ -171,8 +171,8 @@ importers:
|
|||
specifier: ^1.8.1
|
||||
version: 1.8.1
|
||||
content-disposition:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
cookie-parser:
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
|
|
@ -192,8 +192,8 @@ importers:
|
|||
specifier: ^8.6.2
|
||||
version: 8.6.2(express@5.2.1)
|
||||
file-type:
|
||||
specifier: ^22.0.1
|
||||
version: 22.0.1
|
||||
specifier: ^22.0.2
|
||||
version: 22.0.2
|
||||
helmet:
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0
|
||||
|
|
@ -203,9 +203,6 @@ importers:
|
|||
mammoth:
|
||||
specifier: ^1.12.1
|
||||
version: 1.12.1
|
||||
mime-types:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
multer:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
|
|
@ -213,8 +210,8 @@ importers:
|
|||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
nostr-tools:
|
||||
specifier: 2.24.1
|
||||
version: 2.24.1(typescript@6.0.3)
|
||||
specifier: 2.24.3
|
||||
version: 2.24.3(typescript@6.0.3)
|
||||
pino:
|
||||
specifier: ^10.3.1
|
||||
version: 10.3.1
|
||||
|
|
@ -270,9 +267,6 @@ importers:
|
|||
'@types/jsonwebtoken':
|
||||
specifier: ^9.0.10
|
||||
version: 9.0.10
|
||||
'@types/mime-types':
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
'@types/multer':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
|
|
@ -292,8 +286,8 @@ importers:
|
|||
specifier: ^8.5.13
|
||||
version: 8.18.1
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.1.10
|
||||
version: 4.1.10(vitest@4.1.10)
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(vitest@4.1.11)
|
||||
eslint:
|
||||
specifier: ^10.8.1
|
||||
version: 10.8.1(jiti@2.7.0)
|
||||
|
|
@ -327,9 +321,6 @@ importers:
|
|||
'@mantine/core':
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.1(@mantine/hooks@9.5.1(react@19.2.8))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@mantine/form':
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.1(react@19.2.8)
|
||||
'@mantine/hooks':
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.1(react@19.2.8)
|
||||
|
|
@ -355,11 +346,11 @@ importers:
|
|||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
dompurify:
|
||||
specifier: ^3.4.13
|
||||
version: 3.4.13
|
||||
specifier: ^3.4.14
|
||||
version: 3.4.14
|
||||
lucide-react:
|
||||
specifier: ^1.31.0
|
||||
version: 1.31.0(react@19.2.8)
|
||||
specifier: ^1.33.0
|
||||
version: 1.33.0(react@19.2.8)
|
||||
nanoid:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
|
|
@ -401,14 +392,14 @@ importers:
|
|||
specifier: ^4.3.3
|
||||
version: 4.3.3(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.9.1
|
||||
version: 6.9.1
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.11)
|
||||
'@testing-library/react':
|
||||
specifier: ^16.3.2
|
||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@testing-library/user-event':
|
||||
specifier: ^14.6.4
|
||||
version: 14.6.4(@testing-library/dom@10.4.1)
|
||||
specifier: ^14.6.5
|
||||
version: 14.6.5(@testing-library/dom@10.4.1)
|
||||
'@types/dompurify':
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
|
|
@ -449,13 +440,13 @@ importers:
|
|||
specifier: ^8.2.1
|
||||
version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0)
|
||||
vitest:
|
||||
specifier: ^4.1.10
|
||||
version: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(@types/node@26.2.0)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
|
||||
packages:
|
||||
|
||||
'@adobe/css-tools@4.4.4':
|
||||
resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
|
||||
'@adobe/css-tools@4.5.0':
|
||||
resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@14.0.1':
|
||||
resolution: {integrity: sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==}
|
||||
|
|
@ -560,6 +551,11 @@ packages:
|
|||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/parser@7.29.8':
|
||||
resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/plugin-transform-arrow-functions@7.29.7':
|
||||
resolution: {integrity: sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -590,6 +586,10 @@ packages:
|
|||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.29.8':
|
||||
resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2':
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -1005,11 +1005,6 @@ packages:
|
|||
react: ^19.2.0
|
||||
react-dom: ^19.2.0
|
||||
|
||||
'@mantine/form@9.5.1':
|
||||
resolution: {integrity: sha512-WsMZGBTsoG2Y/K+6QK0+GmsJxq5KzWHIzo65lRpkq/SwdenV9G+3m0LstlhI9i2/wQF83GfwP7mCbKMfzf00Eg==}
|
||||
peerDependencies:
|
||||
react: ^19.2.0
|
||||
|
||||
'@mantine/hooks@9.5.1':
|
||||
resolution: {integrity: sha512-2sK9OdWvrzKBOuWxLfQA5qcAJzxpzqNlKumaP7Uq0i7LDBQeY/sYgNiBWLwtW+iZw6fFXwPf0nI7q5VVpvqnNQ==}
|
||||
peerDependencies:
|
||||
|
|
@ -1070,47 +1065,47 @@ packages:
|
|||
resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@openai/codex-sdk@0.147.0':
|
||||
resolution: {integrity: sha512-nJL0maDBZy31uEArs+u46tW22veNdHjfs96AGaFTnI3jF+g8U+a422uaPiDZwEKmyxcNwStTRz6sIh6C7XxGFQ==}
|
||||
'@openai/codex-sdk@0.148.0':
|
||||
resolution: {integrity: sha512-NWTd6ZxuwsuNFqFTpONQCnFVCf++g7b8eafScW5Enpv6v2lCXHizpr07iK9U6RFnLEuAP2KFirVUlZnxxY0j6A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@openai/codex@0.147.0':
|
||||
resolution: {integrity: sha512-EQLEXecAG2ptxI7UpBMo2TR/ga5596/c/OsYF/0LoUDh5JANZ7IoGqlzBEWbuEVQ76JePIbtTW/ihCkp1a7Z3w==}
|
||||
'@openai/codex@0.148.0':
|
||||
resolution: {integrity: sha512-bh5kH9+BMrFaHGmLeoSansPdfRksvr4UXzjQInns/KRO7r8VJ+6AAW+SqUsE8XcG3+OW/mI4EEy8Gpo9UDXGvQ==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
'@openai/codex@0.147.0-darwin-arm64':
|
||||
resolution: {integrity: sha512-BEUVkiOW7kLcRyrMLfAr/h9wF8sRVJyZDy6OHtVn6QGDXiv3BvAZVTY1Pu9xF7KdIdkYXbp4uayN0aDQQaAUJw==}
|
||||
'@openai/codex@0.148.0-darwin-arm64':
|
||||
resolution: {integrity: sha512-xgBPFiF1fHUlRS7HE6wGB56LjBJh16kGD7b4TTbwdVBZNB4QDkTok+vdkAGrfpVkfKcwGNhPSKDgCw+KMZOVug==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@openai/codex@0.147.0-darwin-x64':
|
||||
resolution: {integrity: sha512-Tb8McE5SvJIH0Vs5R6sq7u+quiC931yan2KOOl6km1OdZ82+Wi7eF5XrSFPs5CF7xCgoIK4Vs+byMbT5hN+ZUw==}
|
||||
'@openai/codex@0.148.0-darwin-x64':
|
||||
resolution: {integrity: sha512-qepQolhJutfOp+e9i7L3xsi8aoWeCUiiRq274WMWqRj50rKTrXxsuAgkAwDbqEfT3G5VynhYZuQvDsW37JgdNQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@openai/codex@0.147.0-linux-arm64':
|
||||
resolution: {integrity: sha512-SLC1JXw2TYfr/c3HhrJubyyLelq7vTOLWVmiThFA+z0+WgzCPmaseJ/kzDD3Gge/TO7fCnnj7UcPmC0d2c8XAg==}
|
||||
'@openai/codex@0.148.0-linux-arm64':
|
||||
resolution: {integrity: sha512-51DCd+izzk6n4mMh4w2utWj3lTLhSTnCOEJQfRh0LS9nBDkcYZcK3iSKOST6fByRIlLSXuLO33LlYYA1VPot6A==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@openai/codex@0.147.0-linux-x64':
|
||||
resolution: {integrity: sha512-0W9MBxPpWW0cSkNqrTDN2jR7rzzT7oNMhQY5446lT2Lw5cz5yhDTck4Va9rjkQEm+HlFzP/dmEMSZbXfJsINmw==}
|
||||
'@openai/codex@0.148.0-linux-x64':
|
||||
resolution: {integrity: sha512-uDT9s7AfMr9xLuJX3ZLVWHgHkUpCnZ33CZjZEdVQhrYCIErkDHsCW5TG290nNjaKngK0WxGt5uCcxeUHv9MWWA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@openai/codex@0.147.0-win32-arm64':
|
||||
resolution: {integrity: sha512-e2ZstJ8zT8Rm1nvR7CUVO+Gr3cTChE41+VfOzGhynzDXEoW0wfbjUQbc2bWbh1arG94LMm4y3dqBtUIbSrfeGA==}
|
||||
'@openai/codex@0.148.0-win32-arm64':
|
||||
resolution: {integrity: sha512-a8iOwLzs8UdnlWDHjgK3W/YSBBsUImG8X5XLBjengp3XGJRruhiIsQtUDUOYimCmotKPM4aX7Ub6zjl/KPxMQQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@openai/codex@0.147.0-win32-x64':
|
||||
resolution: {integrity: sha512-oT7Ss5fAPf2fiWE9QNURqZcQGAAawSVxmIUdgPzckq4KFZAM+pRz9JbM4Rr498CjtbNgTOjWvDJ+DXvIBSfOPA==}
|
||||
'@openai/codex@0.148.0-win32-x64':
|
||||
resolution: {integrity: sha512-/Jg8eYw0BqTGNUpnrzzWlK2kbu29NWg7t6pnUDEfxqpTUf+mK8r3okXQn60Zjbk9InYZ4d8SwSjrtOa+i5hSPw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
|
@ -1385,9 +1380,15 @@ packages:
|
|||
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@testing-library/jest-dom@6.9.1':
|
||||
resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
|
||||
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
|
||||
'@testing-library/jest-dom@7.0.1':
|
||||
resolution: {integrity: sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==}
|
||||
engines: {node: '>=22', npm: '>=6', yarn: '>=1'}
|
||||
peerDependencies:
|
||||
'@testing-library/dom': '>=10 <11'
|
||||
vitest: '>= 0.32'
|
||||
peerDependenciesMeta:
|
||||
vitest:
|
||||
optional: true
|
||||
|
||||
'@testing-library/react@16.3.2':
|
||||
resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
|
||||
|
|
@ -1404,8 +1405,8 @@ packages:
|
|||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@testing-library/user-event@14.6.4':
|
||||
resolution: {integrity: sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==}
|
||||
'@testing-library/user-event@14.6.5':
|
||||
resolution: {integrity: sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
peerDependencies:
|
||||
'@testing-library/dom': '>=7.21.4'
|
||||
|
|
@ -1528,9 +1529,6 @@ packages:
|
|||
'@types/methods@1.1.4':
|
||||
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
||||
|
||||
'@types/mime-types@3.0.1':
|
||||
resolution: {integrity: sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==}
|
||||
|
||||
'@types/ms@2.1.0':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
|
|
@ -1679,20 +1677,20 @@ packages:
|
|||
babel-plugin-react-compiler:
|
||||
optional: true
|
||||
|
||||
'@vitest/coverage-v8@4.1.10':
|
||||
resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==}
|
||||
'@vitest/coverage-v8@4.1.11':
|
||||
resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==}
|
||||
peerDependencies:
|
||||
'@vitest/browser': 4.1.10
|
||||
vitest: 4.1.10
|
||||
'@vitest/browser': 4.1.11
|
||||
vitest: 4.1.11
|
||||
peerDependenciesMeta:
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
|
||||
'@vitest/expect@4.1.10':
|
||||
resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==}
|
||||
'@vitest/expect@4.1.11':
|
||||
resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
|
||||
|
||||
'@vitest/mocker@4.1.10':
|
||||
resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==}
|
||||
'@vitest/mocker@4.1.11':
|
||||
resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
|
@ -1702,20 +1700,20 @@ packages:
|
|||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@4.1.10':
|
||||
resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==}
|
||||
'@vitest/pretty-format@4.1.11':
|
||||
resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==}
|
||||
|
||||
'@vitest/runner@4.1.10':
|
||||
resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==}
|
||||
'@vitest/runner@4.1.11':
|
||||
resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==}
|
||||
|
||||
'@vitest/snapshot@4.1.10':
|
||||
resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==}
|
||||
'@vitest/snapshot@4.1.11':
|
||||
resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==}
|
||||
|
||||
'@vitest/spy@4.1.10':
|
||||
resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==}
|
||||
'@vitest/spy@4.1.11':
|
||||
resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==}
|
||||
|
||||
'@vitest/utils@4.1.10':
|
||||
resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==}
|
||||
'@vitest/utils@4.1.11':
|
||||
resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
|
||||
|
||||
'@xmldom/xmldom@0.8.13':
|
||||
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
|
||||
|
|
@ -1860,8 +1858,8 @@ packages:
|
|||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ast-v8-to-istanbul@1.0.4:
|
||||
resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==}
|
||||
ast-v8-to-istanbul@1.0.5:
|
||||
resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==}
|
||||
|
||||
async-exit-hook@2.0.1:
|
||||
resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==}
|
||||
|
|
@ -2151,8 +2149,8 @@ packages:
|
|||
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
|
||||
engines: {'0': node >= 6.0}
|
||||
|
||||
concurrently@10.0.4:
|
||||
resolution: {integrity: sha512-trZql+7l/0+WRAsAnEdctr4+iiOS6ZrViI6H8QWcCF9MFS/LT0dKpe8vluB1to6it+OxSI4VospFTIFMW8DJRw==}
|
||||
concurrently@10.0.5:
|
||||
resolution: {integrity: sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==}
|
||||
engines: {node: '>=22'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -2160,9 +2158,9 @@ packages:
|
|||
resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
content-disposition@2.0.1:
|
||||
resolution: {integrity: sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==}
|
||||
engines: {node: '>=18'}
|
||||
content-disposition@3.0.0:
|
||||
resolution: {integrity: sha512-ZH/0Xs9rMIFWCOmGdmS9eHBTF62qqQYNz4nVjQhkdIO/a0fCP4UIM3mRz/wiqL0L14YgAz/1xio4OaSY4+ON/A==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
content-type@1.0.5:
|
||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||
|
|
@ -2425,8 +2423,8 @@ packages:
|
|||
resolution: {integrity: sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
dompurify@3.4.13:
|
||||
resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==}
|
||||
dompurify@3.4.14:
|
||||
resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==}
|
||||
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
|
@ -2500,8 +2498,8 @@ packages:
|
|||
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
electron@43.4.0:
|
||||
resolution: {integrity: sha512-3qxGF0CeQbiox5oWV1JlbWGQ1VerbmDhTFqW4sJ8h7uqTHniFYPObXJcDna0DMh32et0fFyKzz0YY8lJv3t5jg==}
|
||||
electron@43.4.1:
|
||||
resolution: {integrity: sha512-5b+EuiwkgG5iRcsEL34rimgRpkYp15SsfZOa0pC5kXs0Tb82TH4n95rpQzTZa7yRCbA7tm0WoEbuBL6NaAhAcA==}
|
||||
engines: {node: '>= 22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -2561,8 +2559,8 @@ packages:
|
|||
resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-module-lexer@2.3.0:
|
||||
resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==}
|
||||
es-module-lexer@2.3.2:
|
||||
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
|
|
@ -2743,8 +2741,8 @@ packages:
|
|||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
file-type@22.0.1:
|
||||
resolution: {integrity: sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==}
|
||||
file-type@22.0.2:
|
||||
resolution: {integrity: sha512-0H8TsCUGBLx+V5adH3EY52hTAcyLKbV1D4gq5cIOJ6DnQAHeV9Z2Hhuc5CoBX4YmvB2oL+JIC84z0qO7JsCoNw==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
filelist@1.0.6:
|
||||
|
|
@ -2976,8 +2974,8 @@ packages:
|
|||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
hono@4.13.2:
|
||||
resolution: {integrity: sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==}
|
||||
hono@4.13.3:
|
||||
resolution: {integrity: sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
|
|
@ -3340,10 +3338,6 @@ packages:
|
|||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
klona@2.0.6:
|
||||
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
launder@1.7.1:
|
||||
resolution: {integrity: sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==}
|
||||
|
||||
|
|
@ -3614,8 +3608,8 @@ packages:
|
|||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lucide-react@1.31.0:
|
||||
resolution: {integrity: sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg==}
|
||||
lucide-react@1.33.0:
|
||||
resolution: {integrity: sha512-MTRwMy0ZlL8Ur/vOAiJ9XGHE+kFPC7brq6MxAm0GiGXEBj0qy0jA/pG4N675oSzciO/UCdX8T+5yUQdmDeTLxg==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
|
|
@ -3626,8 +3620,8 @@ packages:
|
|||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
magicast@0.5.3:
|
||||
resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==}
|
||||
magicast@0.5.4:
|
||||
resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==}
|
||||
|
||||
make-dir@4.0.0:
|
||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||
|
|
@ -3928,8 +3922,8 @@ packages:
|
|||
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
nostr-tools@2.24.1:
|
||||
resolution: {integrity: sha512-KdrKjC74n/rr6J3eCSfZj8dcbZFvolHYe4S22SefNZ5YWbhHiB0KL/mmJjEZ0u6B9mZK0YcQtl+WQ46KzwapeQ==}
|
||||
nostr-tools@2.24.3:
|
||||
resolution: {integrity: sha512-8vPgdWuje+H0v2bnxo1/hZOi8VLLMqGjfO+Oxqkx+J22r76rIt/iICKZKBoTXWAP2qTYKCQv89M4A3PRc48JYw==}
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
peerDependenciesMeta:
|
||||
|
|
@ -3967,8 +3961,8 @@ packages:
|
|||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
obug@2.1.3:
|
||||
resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
|
||||
obug@2.1.4:
|
||||
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
on-exit-leak-free@2.1.2:
|
||||
|
|
@ -4808,10 +4802,6 @@ packages:
|
|||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
tinyexec@1.2.4:
|
||||
resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyexec@1.3.0:
|
||||
resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4820,8 +4810,8 @@ packages:
|
|||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinyrainbow@3.1.0:
|
||||
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||
tinyrainbow@3.1.1:
|
||||
resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tldts-core@7.0.30:
|
||||
|
|
@ -5108,20 +5098,20 @@ packages:
|
|||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@4.1.10:
|
||||
resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==}
|
||||
vitest@4.1.11:
|
||||
resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==}
|
||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@opentelemetry/api': ^1.9.0
|
||||
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||
'@vitest/browser-playwright': 4.1.10
|
||||
'@vitest/browser-preview': 4.1.10
|
||||
'@vitest/browser-webdriverio': 4.1.10
|
||||
'@vitest/coverage-istanbul': 4.1.10
|
||||
'@vitest/coverage-v8': 4.1.10
|
||||
'@vitest/ui': 4.1.10
|
||||
'@vitest/browser-playwright': 4.1.11
|
||||
'@vitest/browser-preview': 4.1.11
|
||||
'@vitest/browser-webdriverio': 4.1.11
|
||||
'@vitest/coverage-istanbul': 4.1.11
|
||||
'@vitest/coverage-v8': 4.1.11
|
||||
'@vitest/ui': 4.1.11
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
|
@ -5315,7 +5305,7 @@ packages:
|
|||
|
||||
snapshots:
|
||||
|
||||
'@adobe/css-tools@4.4.4': {}
|
||||
'@adobe/css-tools@4.5.0': {}
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@14.0.1':
|
||||
dependencies:
|
||||
|
|
@ -5405,7 +5395,7 @@ snapshots:
|
|||
'@babel/helper-module-imports@7.29.7':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
'@babel/types': 7.29.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -5443,6 +5433,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@babel/types': 7.29.7
|
||||
|
||||
'@babel/parser@7.29.8':
|
||||
dependencies:
|
||||
'@babel/types': 7.29.8
|
||||
|
||||
'@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
|
|
@ -5480,6 +5474,11 @@ snapshots:
|
|||
'@babel/helper-string-parser': 7.29.7
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
|
||||
'@babel/types@7.29.8':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.29.7
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@borewit/text-codec@0.2.2': {}
|
||||
|
|
@ -5791,9 +5790,9 @@ snapshots:
|
|||
|
||||
'@floating-ui/utils@0.2.12': {}
|
||||
|
||||
'@hono/node-server@2.1.1(hono@4.13.2)':
|
||||
'@hono/node-server@2.1.1(hono@4.13.3)':
|
||||
dependencies:
|
||||
hono: 4.13.2
|
||||
hono: 4.13.3
|
||||
|
||||
'@humanfs/core@0.19.2':
|
||||
dependencies:
|
||||
|
|
@ -5870,13 +5869,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@mantine/form@9.5.1(react@19.2.8)':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
fast-deep-equal: 3.1.3
|
||||
klona: 2.0.6
|
||||
react: 19.2.8
|
||||
|
||||
'@mantine/hooks@9.5.1(react@19.2.8)':
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
|
|
@ -5903,7 +5895,7 @@ snapshots:
|
|||
|
||||
'@modelcontextprotocol/sdk@1.30.0(zod@4.4.3)':
|
||||
dependencies:
|
||||
'@hono/node-server': 2.1.1(hono@4.13.2)
|
||||
'@hono/node-server': 2.1.1(hono@4.13.3)
|
||||
ajv: 8.20.0
|
||||
ajv-formats: 3.0.1(ajv@8.20.0)
|
||||
content-type: 1.0.5
|
||||
|
|
@ -5913,7 +5905,7 @@ snapshots:
|
|||
eventsource-parser: 3.1.1
|
||||
express: 5.2.1
|
||||
express-rate-limit: 8.6.2(express@5.2.1)
|
||||
hono: 4.13.2
|
||||
hono: 4.13.3
|
||||
jose: 6.2.9
|
||||
json-schema-typed: 8.0.2
|
||||
pkce-challenge: 5.0.1
|
||||
|
|
@ -5937,35 +5929,35 @@ snapshots:
|
|||
|
||||
'@noble/hashes@2.2.0': {}
|
||||
|
||||
'@openai/codex-sdk@0.147.0':
|
||||
'@openai/codex-sdk@0.148.0':
|
||||
dependencies:
|
||||
'@openai/codex': 0.147.0
|
||||
'@openai/codex': 0.148.0
|
||||
|
||||
'@openai/codex@0.147.0':
|
||||
'@openai/codex@0.148.0':
|
||||
optionalDependencies:
|
||||
'@openai/codex-darwin-arm64': '@openai/codex@0.147.0-darwin-arm64'
|
||||
'@openai/codex-darwin-x64': '@openai/codex@0.147.0-darwin-x64'
|
||||
'@openai/codex-linux-arm64': '@openai/codex@0.147.0-linux-arm64'
|
||||
'@openai/codex-linux-x64': '@openai/codex@0.147.0-linux-x64'
|
||||
'@openai/codex-win32-arm64': '@openai/codex@0.147.0-win32-arm64'
|
||||
'@openai/codex-win32-x64': '@openai/codex@0.147.0-win32-x64'
|
||||
'@openai/codex-darwin-arm64': '@openai/codex@0.148.0-darwin-arm64'
|
||||
'@openai/codex-darwin-x64': '@openai/codex@0.148.0-darwin-x64'
|
||||
'@openai/codex-linux-arm64': '@openai/codex@0.148.0-linux-arm64'
|
||||
'@openai/codex-linux-x64': '@openai/codex@0.148.0-linux-x64'
|
||||
'@openai/codex-win32-arm64': '@openai/codex@0.148.0-win32-arm64'
|
||||
'@openai/codex-win32-x64': '@openai/codex@0.148.0-win32-x64'
|
||||
|
||||
'@openai/codex@0.147.0-darwin-arm64':
|
||||
'@openai/codex@0.148.0-darwin-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.147.0-darwin-x64':
|
||||
'@openai/codex@0.148.0-darwin-x64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.147.0-linux-arm64':
|
||||
'@openai/codex@0.148.0-linux-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.147.0-linux-x64':
|
||||
'@openai/codex@0.148.0-linux-x64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.147.0-win32-arm64':
|
||||
'@openai/codex@0.148.0-win32-arm64':
|
||||
optional: true
|
||||
|
||||
'@openai/codex@0.147.0-win32-x64':
|
||||
'@openai/codex@0.148.0-win32-x64':
|
||||
optional: true
|
||||
|
||||
'@oxc-project/types@0.144.0': {}
|
||||
|
|
@ -6175,14 +6167,17 @@ snapshots:
|
|||
picocolors: 1.1.1
|
||||
pretty-format: 27.5.1
|
||||
|
||||
'@testing-library/jest-dom@6.9.1':
|
||||
'@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.11)':
|
||||
dependencies:
|
||||
'@adobe/css-tools': 4.4.4
|
||||
'@adobe/css-tools': 4.5.0
|
||||
'@testing-library/dom': 10.4.1
|
||||
aria-query: 5.3.2
|
||||
css.escape: 1.5.1
|
||||
dom-accessibility-api: 0.6.3
|
||||
picocolors: 1.1.1
|
||||
redent: 3.0.0
|
||||
optionalDependencies:
|
||||
vitest: 4.1.11(@types/node@26.2.0)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
|
||||
'@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
||||
dependencies:
|
||||
|
|
@ -6194,7 +6189,7 @@ snapshots:
|
|||
'@types/react': 19.2.18
|
||||
'@types/react-dom': 19.2.4(@types/react@19.2.18)
|
||||
|
||||
'@testing-library/user-event@14.6.4(@testing-library/dom@10.4.1)':
|
||||
'@testing-library/user-event@14.6.5(@testing-library/dom@10.4.1)':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.1
|
||||
|
||||
|
|
@ -6281,7 +6276,7 @@ snapshots:
|
|||
|
||||
'@types/dompurify@3.2.0':
|
||||
dependencies:
|
||||
dompurify: 3.4.13
|
||||
dompurify: 3.4.14
|
||||
|
||||
'@types/esrecurse@4.3.1': {}
|
||||
|
||||
|
|
@ -6333,8 +6328,6 @@ snapshots:
|
|||
|
||||
'@types/methods@1.1.4': {}
|
||||
|
||||
'@types/mime-types@3.0.1': {}
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/multer@2.2.0':
|
||||
|
|
@ -6517,60 +6510,60 @@ snapshots:
|
|||
'@rolldown/pluginutils': 1.0.1
|
||||
vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0)
|
||||
|
||||
'@vitest/coverage-v8@4.1.10(vitest@4.1.10)':
|
||||
'@vitest/coverage-v8@4.1.11(vitest@4.1.11)':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.1.10
|
||||
ast-v8-to-istanbul: 1.0.4
|
||||
'@vitest/utils': 4.1.11
|
||||
ast-v8-to-istanbul: 1.0.5
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-reports: 3.2.0
|
||||
magicast: 0.5.3
|
||||
obug: 2.1.3
|
||||
magicast: 0.5.4
|
||||
obug: 2.1.4
|
||||
std-env: 4.2.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
tinyrainbow: 3.1.1
|
||||
vitest: 4.1.11(@types/node@26.2.0)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
|
||||
'@vitest/expect@4.1.10':
|
||||
'@vitest/expect@4.1.11':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/spy': 4.1.10
|
||||
'@vitest/utils': 4.1.10
|
||||
'@vitest/spy': 4.1.11
|
||||
'@vitest/utils': 4.1.11
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
tinyrainbow: 3.1.1
|
||||
|
||||
'@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))':
|
||||
'@vitest/mocker@4.1.11(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.10
|
||||
'@vitest/spy': 4.1.11
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0)
|
||||
|
||||
'@vitest/pretty-format@4.1.10':
|
||||
'@vitest/pretty-format@4.1.11':
|
||||
dependencies:
|
||||
tinyrainbow: 3.1.0
|
||||
tinyrainbow: 3.1.1
|
||||
|
||||
'@vitest/runner@4.1.10':
|
||||
'@vitest/runner@4.1.11':
|
||||
dependencies:
|
||||
'@vitest/utils': 4.1.10
|
||||
'@vitest/utils': 4.1.11
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@4.1.10':
|
||||
'@vitest/snapshot@4.1.11':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.1.10
|
||||
'@vitest/utils': 4.1.10
|
||||
'@vitest/pretty-format': 4.1.11
|
||||
'@vitest/utils': 4.1.11
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@4.1.10': {}
|
||||
'@vitest/spy@4.1.11': {}
|
||||
|
||||
'@vitest/utils@4.1.10':
|
||||
'@vitest/utils@4.1.11':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.1.10
|
||||
'@vitest/pretty-format': 4.1.11
|
||||
convert-source-map: 2.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
tinyrainbow: 3.1.1
|
||||
|
||||
'@xmldom/xmldom@0.8.13': {}
|
||||
|
||||
|
|
@ -6788,7 +6781,7 @@ snapshots:
|
|||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-v8-to-istanbul@1.0.4:
|
||||
ast-v8-to-istanbul@1.0.5:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
estree-walker: 3.0.3
|
||||
|
|
@ -7082,7 +7075,7 @@ snapshots:
|
|||
readable-stream: 3.6.2
|
||||
typedarray: 0.0.6
|
||||
|
||||
concurrently@10.0.4:
|
||||
concurrently@10.0.5:
|
||||
dependencies:
|
||||
chalk: 5.6.2
|
||||
rxjs: 7.8.2
|
||||
|
|
@ -7093,7 +7086,7 @@ snapshots:
|
|||
|
||||
content-disposition@1.1.0: {}
|
||||
|
||||
content-disposition@2.0.1: {}
|
||||
content-disposition@3.0.0: {}
|
||||
|
||||
content-type@1.0.5: {}
|
||||
|
||||
|
|
@ -7335,7 +7328,7 @@ snapshots:
|
|||
dependencies:
|
||||
domelementtype: 3.0.0
|
||||
|
||||
dompurify@3.4.13:
|
||||
dompurify@3.4.14:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
|
|
@ -7461,7 +7454,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron@43.4.0:
|
||||
electron@43.4.1:
|
||||
dependencies:
|
||||
'@electron-internal/extract-zip': 1.0.5
|
||||
'@electron/get': 5.1.0
|
||||
|
|
@ -7576,7 +7569,7 @@ snapshots:
|
|||
iterator.prototype: 1.1.5
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
es-module-lexer@2.3.0: {}
|
||||
es-module-lexer@2.3.2: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
|
|
@ -7842,7 +7835,7 @@ snapshots:
|
|||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
|
||||
file-type@22.0.1:
|
||||
file-type@22.0.2:
|
||||
dependencies:
|
||||
'@tokenizer/inflate': 0.4.1
|
||||
strtok3: 10.3.5
|
||||
|
|
@ -8141,7 +8134,7 @@ snapshots:
|
|||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
hono@4.13.2: {}
|
||||
hono@4.13.3: {}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
dependencies:
|
||||
|
|
@ -8523,8 +8516,6 @@ snapshots:
|
|||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
klona@2.0.6: {}
|
||||
|
||||
launder@1.7.1:
|
||||
dependencies:
|
||||
dayjs: 1.11.21
|
||||
|
|
@ -8728,7 +8719,7 @@ snapshots:
|
|||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
lucide-react@1.31.0(react@19.2.8):
|
||||
lucide-react@1.33.0(react@19.2.8):
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
|
||||
|
|
@ -8738,10 +8729,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
magicast@0.5.3:
|
||||
magicast@0.5.4:
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.7
|
||||
'@babel/types': 7.29.7
|
||||
'@babel/parser': 7.29.8
|
||||
'@babel/types': 7.29.8
|
||||
source-map-js: 1.2.1
|
||||
|
||||
make-dir@4.0.0:
|
||||
|
|
@ -9229,7 +9220,7 @@ snapshots:
|
|||
|
||||
normalize-url@6.1.0: {}
|
||||
|
||||
nostr-tools@2.24.1(typescript@6.0.3):
|
||||
nostr-tools@2.24.3(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@noble/ciphers': 2.1.1
|
||||
'@noble/curves': 2.0.1
|
||||
|
|
@ -9279,7 +9270,7 @@ snapshots:
|
|||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
obug@2.1.3: {}
|
||||
obug@2.1.4: {}
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
|
|
@ -10284,8 +10275,6 @@ snapshots:
|
|||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@1.2.4: {}
|
||||
|
||||
tinyexec@1.3.0: {}
|
||||
|
||||
tinyglobby@0.2.17:
|
||||
|
|
@ -10293,7 +10282,7 @@ snapshots:
|
|||
fdir: 6.5.0(picomatch@4.0.5)
|
||||
picomatch: 4.0.5
|
||||
|
||||
tinyrainbow@3.1.0: {}
|
||||
tinyrainbow@3.1.1: {}
|
||||
|
||||
tldts-core@7.0.30: {}
|
||||
|
||||
|
|
@ -10580,31 +10569,31 @@ snapshots:
|
|||
tsx: 4.23.12
|
||||
yaml: 2.9.0
|
||||
|
||||
vitest@4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0)):
|
||||
vitest@4.1.11(@types/node@26.2.0)(@vitest/coverage-v8@4.1.11)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.10
|
||||
'@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
'@vitest/pretty-format': 4.1.10
|
||||
'@vitest/runner': 4.1.10
|
||||
'@vitest/snapshot': 4.1.10
|
||||
'@vitest/spy': 4.1.10
|
||||
'@vitest/utils': 4.1.10
|
||||
es-module-lexer: 2.3.0
|
||||
'@vitest/expect': 4.1.11
|
||||
'@vitest/mocker': 4.1.11(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0))
|
||||
'@vitest/pretty-format': 4.1.11
|
||||
'@vitest/runner': 4.1.11
|
||||
'@vitest/snapshot': 4.1.11
|
||||
'@vitest/spy': 4.1.11
|
||||
'@vitest/utils': 4.1.11
|
||||
es-module-lexer: 2.3.2
|
||||
expect-type: 1.4.0
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.3
|
||||
obug: 2.1.4
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.5
|
||||
std-env: 4.2.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.2.4
|
||||
tinyexec: 1.3.0
|
||||
tinyglobby: 0.2.17
|
||||
tinyrainbow: 3.1.0
|
||||
tinyrainbow: 3.1.1
|
||||
vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.26))(tsx@4.23.12)(yaml@2.9.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 26.2.0
|
||||
'@vitest/coverage-v8': 4.1.10(vitest@4.1.10)
|
||||
'@vitest/coverage-v8': 4.1.11(vitest@4.1.11)
|
||||
jsdom: 29.1.1(@noble/hashes@2.2.0)
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
|
|
|||
154
scripts/check-actions-pinned.mjs
Normal file
154
scripts/check-actions-pinned.mjs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
#!/usr/bin/env node
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const FULL_COMMIT_SHA = /^[0-9a-f]{40}$/;
|
||||
const FULL_DOCKER_DIGEST = /^docker:\/\/[^@\s]+@sha256:[0-9a-f]{64}$/;
|
||||
const RELEASE_COMMENT = /^v?\d+(?:\.\d+){0,2}(?:[-+][0-9A-Za-z.-]+)?$/;
|
||||
|
||||
function parseUsesLine(line) {
|
||||
const match = line.match(/^\s*(?:-\s*)?uses:\s*(.+?)\s*$/);
|
||||
if (!match) return null;
|
||||
|
||||
const declaration = match[1].trim();
|
||||
let reference;
|
||||
let comment = '';
|
||||
|
||||
if (declaration.startsWith("'") || declaration.startsWith('"')) {
|
||||
const quote = declaration[0];
|
||||
const closingQuote = declaration.indexOf(quote, 1);
|
||||
if (closingQuote < 0) return { reference: declaration, comment };
|
||||
reference = declaration.slice(1, closingQuote);
|
||||
const remainder = declaration.slice(closingQuote + 1).trim();
|
||||
if (remainder.startsWith('#')) comment = remainder.slice(1).trim();
|
||||
} else {
|
||||
const commentIndex = declaration.search(/\s+#/);
|
||||
reference = (commentIndex >= 0 ? declaration.slice(0, commentIndex) : declaration).trim();
|
||||
if (commentIndex >= 0)
|
||||
comment = declaration
|
||||
.slice(commentIndex)
|
||||
.replace(/^\s+#\s*/, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
return { reference, comment };
|
||||
}
|
||||
|
||||
function releaseCommentViolation(file, line, reference, comment) {
|
||||
if (RELEASE_COMMENT.test(comment)) return null;
|
||||
return {
|
||||
file,
|
||||
line,
|
||||
reference,
|
||||
message: 'immutable external action pin must include a trailing release comment',
|
||||
};
|
||||
}
|
||||
|
||||
export function findActionPinningViolations(files) {
|
||||
const violations = [];
|
||||
|
||||
for (const [file, content] of Object.entries(files).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
for (const [index, sourceLine] of content.split(/\r?\n/).entries()) {
|
||||
const parsed = parseUsesLine(sourceLine);
|
||||
if (!parsed) continue;
|
||||
|
||||
const { reference, comment } = parsed;
|
||||
const line = index + 1;
|
||||
|
||||
if (reference.startsWith('./')) continue;
|
||||
|
||||
if (reference.startsWith('docker://')) {
|
||||
if (!FULL_DOCKER_DIGEST.test(reference)) {
|
||||
violations.push({
|
||||
file,
|
||||
line,
|
||||
reference,
|
||||
message: 'Docker action must use a full sha256 digest',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const commentViolation = releaseCommentViolation(file, line, reference, comment);
|
||||
if (commentViolation) violations.push(commentViolation);
|
||||
continue;
|
||||
}
|
||||
|
||||
const separator = reference.lastIndexOf('@');
|
||||
const action = separator >= 0 ? reference.slice(0, separator) : reference;
|
||||
const revision = separator >= 0 ? reference.slice(separator + 1) : '';
|
||||
if (!action.includes('/') || !FULL_COMMIT_SHA.test(revision)) {
|
||||
violations.push({
|
||||
file,
|
||||
line,
|
||||
reference,
|
||||
message: 'external action must use a full 40-character commit SHA',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const commentViolation = releaseCommentViolation(file, line, reference, comment);
|
||||
if (commentViolation) violations.push(commentViolation);
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function findDependabotConfigurationViolations(content) {
|
||||
return /package-ecosystem:\s*['"]?github-actions['"]?(?:\s|$)/.test(content)
|
||||
? []
|
||||
: ['Dependabot must retain a github-actions ecosystem entry'];
|
||||
}
|
||||
|
||||
function collectYamlFiles(directory) {
|
||||
if (!existsSync(directory)) return {};
|
||||
const files = {};
|
||||
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
Object.assign(files, collectYamlFiles(absolutePath));
|
||||
} else if (/\.ya?ml$/i.test(entry.name)) {
|
||||
const relativePath = path.relative(process.cwd(), absolutePath).replaceAll('\\', '/');
|
||||
files[relativePath] = readFileSync(absolutePath, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export function runActionPinningCheck() {
|
||||
const workflowFiles = {
|
||||
...collectYamlFiles(path.resolve('.github/workflows')),
|
||||
...collectYamlFiles(path.resolve('.github/actions')),
|
||||
};
|
||||
const violations = findActionPinningViolations(workflowFiles);
|
||||
const dependabotPath = path.resolve('.github/dependabot.yml');
|
||||
const dependabotViolations = existsSync(dependabotPath)
|
||||
? findDependabotConfigurationViolations(readFileSync(dependabotPath, 'utf8'))
|
||||
: ['Dependabot configuration is missing'];
|
||||
|
||||
if (violations.length > 0 || dependabotViolations.length > 0) {
|
||||
console.error('Immutable GitHub Actions check failed.');
|
||||
for (const violation of violations) {
|
||||
console.error(
|
||||
`- ${violation.file}:${violation.line} ${violation.reference}: ${violation.message}`
|
||||
);
|
||||
}
|
||||
for (const message of dependabotViolations)
|
||||
console.error(`- .github/dependabot.yml: ${message}`);
|
||||
process.exitCode = 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Immutable GitHub Actions check passed (${Object.keys(workflowFiles).length} YAML files scanned).`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function isDirectExecution() {
|
||||
return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
}
|
||||
|
||||
if (isDirectExecution()) runActionPinningCheck();
|
||||
103
scripts/check-actions-pinned.test.mjs
Normal file
103
scripts/check-actions-pinned.test.mjs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
findActionPinningViolations,
|
||||
findDependabotConfigurationViolations,
|
||||
} from './check-actions-pinned.mjs';
|
||||
|
||||
const SHA = '3d3c42e5aac5ba805825da76410c181273ba90b1';
|
||||
const DIGEST = 'a'.repeat(64);
|
||||
|
||||
test('accepts immutable external action pins with release comments', () => {
|
||||
assert.deepEqual(
|
||||
findActionPinningViolations({
|
||||
'.github/workflows/ci.yml': `steps:\n - uses: actions/checkout@${SHA} # v7.0.1\n`,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects mutable tags, branches, short SHAs, and missing release comments', () => {
|
||||
assert.deepEqual(
|
||||
findActionPinningViolations({
|
||||
'.github/workflows/ci.yml': [
|
||||
'steps:',
|
||||
' - uses: actions/checkout@v7',
|
||||
' - uses: actions/setup-node@main',
|
||||
' - uses: actions/upload-artifact@043fb46',
|
||||
` - uses: pnpm/action-setup@${SHA}`,
|
||||
].join('\n'),
|
||||
}),
|
||||
[
|
||||
{
|
||||
file: '.github/workflows/ci.yml',
|
||||
line: 2,
|
||||
reference: 'actions/checkout@v7',
|
||||
message: 'external action must use a full 40-character commit SHA',
|
||||
},
|
||||
{
|
||||
file: '.github/workflows/ci.yml',
|
||||
line: 3,
|
||||
reference: 'actions/setup-node@main',
|
||||
message: 'external action must use a full 40-character commit SHA',
|
||||
},
|
||||
{
|
||||
file: '.github/workflows/ci.yml',
|
||||
line: 4,
|
||||
reference: 'actions/upload-artifact@043fb46',
|
||||
message: 'external action must use a full 40-character commit SHA',
|
||||
},
|
||||
{
|
||||
file: '.github/workflows/ci.yml',
|
||||
line: 5,
|
||||
reference: `pnpm/action-setup@${SHA}`,
|
||||
message: 'immutable external action pin must include a trailing release comment',
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('allows local actions without a commit pin', () => {
|
||||
assert.deepEqual(
|
||||
findActionPinningViolations({
|
||||
'.github/workflows/ci.yml': 'steps:\n - uses: ./.github/actions/setup\n',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('requires immutable digests for Docker actions', () => {
|
||||
assert.deepEqual(
|
||||
findActionPinningViolations({
|
||||
'.github/actions/example/action.yml': [
|
||||
'runs:',
|
||||
' using: docker',
|
||||
' image: docker://alpine:3.22',
|
||||
' - uses: docker://alpine:3.22',
|
||||
` - uses: docker://alpine@sha256:${DIGEST} # 3.22.1`,
|
||||
].join('\n'),
|
||||
}),
|
||||
[
|
||||
{
|
||||
file: '.github/actions/example/action.yml',
|
||||
line: 4,
|
||||
reference: 'docker://alpine:3.22',
|
||||
message: 'Docker action must use a full sha256 digest',
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('requires Dependabot maintenance for GitHub Actions', () => {
|
||||
assert.deepEqual(
|
||||
findDependabotConfigurationViolations("version: 2\nupdates:\n - package-ecosystem: 'npm'\n"),
|
||||
['Dependabot must retain a github-actions ecosystem entry']
|
||||
);
|
||||
assert.deepEqual(
|
||||
findDependabotConfigurationViolations(
|
||||
"version: 2\nupdates:\n - package-ecosystem: 'github-actions'\n directory: '/'\n"
|
||||
),
|
||||
[]
|
||||
);
|
||||
});
|
||||
249
scripts/check-coverage-policy.mjs
Normal file
249
scripts/check-coverage-policy.mjs
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
#!/usr/bin/env node
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { coverageExceptionErrors } from './coverage-policy-utils.mjs';
|
||||
|
||||
const EXPECTED_PACKAGES = ['server', 'web', 'cli', 'mcp', 'desktop'];
|
||||
const METRICS = ['lines', 'branches', 'functions', 'statements'];
|
||||
const COMMIT_SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
|
||||
const GLOB_PATTERN = /[*?[\]{}!]/;
|
||||
|
||||
export function compareCoveragePolicy(current, baseline) {
|
||||
const errors = [];
|
||||
for (const baselinePackage of baseline?.packages ?? []) {
|
||||
const currentPackage = current.packages?.find(({ id }) => id === baselinePackage.id);
|
||||
if (!currentPackage) {
|
||||
errors.push(`coverage package ${baselinePackage.id} cannot be removed`);
|
||||
continue;
|
||||
}
|
||||
for (const baselineBoundary of baselinePackage.boundaries ?? []) {
|
||||
const fullId = `${baselinePackage.id}/${baselineBoundary.id}`;
|
||||
const currentBoundary = currentPackage.boundaries?.find(
|
||||
({ id }) => id === baselineBoundary.id
|
||||
);
|
||||
if (!currentBoundary) {
|
||||
errors.push(`coverage boundary ${fullId} cannot be removed`);
|
||||
continue;
|
||||
}
|
||||
for (const metric of METRICS) {
|
||||
if (currentBoundary.thresholds?.[metric] < baselineBoundary.thresholds?.[metric]) {
|
||||
errors.push(`${fullId} ${metric} threshold cannot decrease`);
|
||||
}
|
||||
}
|
||||
for (const pattern of baselineBoundary.include ?? []) {
|
||||
if (!currentBoundary.include?.includes(pattern)) {
|
||||
errors.push(`${fullId} cannot remove governed include pattern ${pattern}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const field of ['testFiles', 'triggerPatterns']) {
|
||||
for (const input of baselinePackage.runner?.[field] ?? []) {
|
||||
if (!currentPackage.runner?.[field]?.includes(input)) {
|
||||
errors.push(`${baselinePackage.id} cannot remove governed runner ${field} input ${input}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateCoveragePolicy({
|
||||
policy,
|
||||
baselinePolicy,
|
||||
packageJson,
|
||||
workflow,
|
||||
configs,
|
||||
fileExists = () => true,
|
||||
today = new Date().toISOString().slice(0, 10),
|
||||
}) {
|
||||
const errors = [];
|
||||
const packageIds = policy.packages?.map(({ id }) => id) ?? [];
|
||||
const coverageJob = workflow.match(
|
||||
/(?:^|\n) {2}critical-path-coverage:\n[\s\S]*?(?=\n {2}[a-zA-Z0-9_-]+:\n|$)/
|
||||
)?.[0];
|
||||
|
||||
if (policy.schemaVersion !== 'critical-path-coverage/v1') {
|
||||
errors.push('coverage policy must use critical-path-coverage/v1');
|
||||
}
|
||||
if (JSON.stringify(packageIds) !== JSON.stringify(EXPECTED_PACKAGES)) {
|
||||
errors.push(`coverage policy packages must be ${EXPECTED_PACKAGES.join(', ')}`);
|
||||
}
|
||||
for (const metric of METRICS) {
|
||||
if (policy.longTermTarget?.[metric] < 80) {
|
||||
errors.push(`long-term ${metric} target must be at least 80%`);
|
||||
}
|
||||
}
|
||||
|
||||
const boundaryIds = new Set();
|
||||
for (const packagePolicy of policy.packages ?? []) {
|
||||
if (packagePolicy.report !== `coverage/${packagePolicy.id}/coverage-summary.json`) {
|
||||
errors.push(`${packagePolicy.id} must use its canonical machine-readable coverage summary`);
|
||||
}
|
||||
for (const testFile of packagePolicy.runner?.testFiles ?? []) {
|
||||
if (
|
||||
path.posix.normalize(testFile) !== testFile ||
|
||||
path.posix.isAbsolute(testFile) ||
|
||||
GLOB_PATTERN.test(testFile) ||
|
||||
!/\.test\.[cm]?[jt]sx?$/.test(testFile)
|
||||
) {
|
||||
errors.push(`${packagePolicy.id} coverage test files must be exact test paths`);
|
||||
}
|
||||
if (!fileExists(`${packagePolicy.id}/${testFile}`)) {
|
||||
errors.push(`${packagePolicy.id} governed coverage test file does not exist: ${testFile}`);
|
||||
}
|
||||
}
|
||||
for (const boundary of packagePolicy.boundaries ?? []) {
|
||||
const fullId = `${packagePolicy.id}/${boundary.id}`;
|
||||
if (boundaryIds.has(fullId)) errors.push(`duplicate boundary ${fullId}`);
|
||||
boundaryIds.add(fullId);
|
||||
if (
|
||||
!boundary.description ||
|
||||
!Array.isArray(boundary.include) ||
|
||||
boundary.include.length === 0
|
||||
) {
|
||||
errors.push(`${fullId} requires a description and include patterns`);
|
||||
}
|
||||
for (const metric of METRICS) {
|
||||
const threshold = boundary.thresholds?.[metric];
|
||||
if (typeof threshold !== 'number' || threshold <= 0 || threshold > 100) {
|
||||
errors.push(`${fullId} ${metric} threshold must be between 0 and 100`);
|
||||
}
|
||||
}
|
||||
for (const exception of boundary.exceptions ?? []) {
|
||||
errors.push(...coverageExceptionErrors(exception, fullId, today));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!packageJson.scripts?.['test:coverage']?.includes('scripts/run-coverage.mjs')) {
|
||||
errors.push('package.json must expose the documented test:coverage command');
|
||||
}
|
||||
if (!packageJson.devDependencies?.['@vitest/coverage-v8']) {
|
||||
errors.push('the root workspace must own @vitest/coverage-v8');
|
||||
}
|
||||
if (!workflow.includes('critical-path-coverage:')) {
|
||||
errors.push('CI must define the Critical Path Coverage job');
|
||||
}
|
||||
if (!workflow.includes('needs.select-tests.outputs.coverage_packages')) {
|
||||
errors.push('CI coverage must use the deterministic package selection output');
|
||||
}
|
||||
if (!workflow.includes('pnpm test:coverage --packages')) {
|
||||
errors.push('CI must execute the documented coverage command');
|
||||
}
|
||||
if (!workflow.includes('COVERAGE_BASE_REF:')) {
|
||||
errors.push('CI must compare coverage policy and changed critical files with the base commit');
|
||||
}
|
||||
if (!workflow.includes('needs.select-tests.outputs.base_sha')) {
|
||||
errors.push('CI coverage must use the selector base SHA for every event type');
|
||||
}
|
||||
if (!coverageJob?.includes('fetch-depth: 0')) {
|
||||
errors.push('CI coverage checkout must fetch the base commit used by coverage ratchets');
|
||||
}
|
||||
if (!workflow.includes('if: always() && needs.select-tests.outputs.coverage_packages')) {
|
||||
errors.push('CI must preserve coverage artifacts after a failed ratchet');
|
||||
}
|
||||
if (!workflow.includes('actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a')) {
|
||||
errors.push('CI must upload coverage with the immutable approved action');
|
||||
}
|
||||
|
||||
for (const packageId of EXPECTED_PACKAGES) {
|
||||
const config = configs[packageId] ?? '';
|
||||
if (!config.includes("provider: 'v8'") || !config.includes('all: true')) {
|
||||
errors.push(`${packageId} coverage must use V8 and include untested source`);
|
||||
}
|
||||
if (!config.includes("'src/**/*.d.ts'")) {
|
||||
errors.push(`${packageId} coverage must exclude type declarations`);
|
||||
}
|
||||
for (const pattern of [
|
||||
'src/**/__fixtures__/**',
|
||||
'src/**/fixtures/**',
|
||||
'src/**/generated/**',
|
||||
'src/**/*.generated.*',
|
||||
'src/**/types.ts',
|
||||
'src/types/**/*.ts',
|
||||
]) {
|
||||
if (!config.includes(pattern)) errors.push(`${packageId} coverage must exclude ${pattern}`);
|
||||
}
|
||||
}
|
||||
for (const packageId of ['server', 'web']) {
|
||||
if (
|
||||
(policy.packages.find(({ id }) => id === packageId)?.runner?.testFiles?.length ?? 0) === 0
|
||||
) {
|
||||
errors.push(`${packageId} critical coverage must use exact test files`);
|
||||
}
|
||||
}
|
||||
|
||||
errors.push(...compareCoveragePolicy(policy, baselinePolicy));
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function readBaselinePolicy(repoRoot) {
|
||||
const baselineRef = process.env.COVERAGE_BASE_REF;
|
||||
if (!baselineRef) return undefined;
|
||||
if (!COMMIT_SHA_PATTERN.test(baselineRef)) {
|
||||
throw new Error('COVERAGE_BASE_REF must be a hexadecimal commit ID.');
|
||||
}
|
||||
execFileSync('git', ['cat-file', '-e', `${baselineRef}^{commit}`], {
|
||||
cwd: repoRoot,
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
});
|
||||
let baselineText;
|
||||
try {
|
||||
baselineText = execFileSync(
|
||||
'git',
|
||||
['show', `${baselineRef}:docs/testing/critical-path-coverage.json`],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(baselineText);
|
||||
}
|
||||
|
||||
async function main(repoRoot = process.cwd()) {
|
||||
const [policy, packageJson, workflow, ...configTexts] = await Promise.all([
|
||||
readFile(path.join(repoRoot, 'docs/testing/critical-path-coverage.json'), 'utf8').then(
|
||||
JSON.parse
|
||||
),
|
||||
readFile(path.join(repoRoot, 'package.json'), 'utf8').then(JSON.parse),
|
||||
readFile(path.join(repoRoot, '.github/workflows/ci.yml'), 'utf8'),
|
||||
...EXPECTED_PACKAGES.map((packageId) =>
|
||||
readFile(path.join(repoRoot, packageId, 'vitest.config.ts'), 'utf8')
|
||||
),
|
||||
]);
|
||||
const configs = Object.fromEntries(
|
||||
EXPECTED_PACKAGES.map((packageId, index) => [packageId, configTexts[index]])
|
||||
);
|
||||
const errors = validateCoveragePolicy({
|
||||
policy,
|
||||
baselinePolicy: readBaselinePolicy(repoRoot),
|
||||
packageJson,
|
||||
workflow,
|
||||
configs,
|
||||
fileExists: (relativePath) => existsSync(path.join(repoRoot, relativePath)),
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Coverage policy check failed:\n- ${errors.join('\n- ')}`);
|
||||
}
|
||||
console.log(
|
||||
`Critical-path coverage policy covers ${EXPECTED_PACKAGES.length} packages and ${policy.packages.reduce((sum, packagePolicy) => sum + packagePolicy.boundaries.length, 0)} boundaries.`
|
||||
);
|
||||
}
|
||||
|
||||
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
||||
if (isMain) {
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
128
scripts/check-coverage-policy.test.mjs
Normal file
128
scripts/check-coverage-policy.test.mjs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { compareCoveragePolicy, validateCoveragePolicy } from './check-coverage-policy.mjs';
|
||||
|
||||
const packages = ['server', 'web', 'cli', 'mcp', 'desktop'].map((id) => ({
|
||||
id,
|
||||
report: `coverage/${id}/coverage-summary.json`,
|
||||
...(id === 'server' || id === 'web'
|
||||
? { runner: { testFiles: ['src/__tests__/critical.test.ts'] } }
|
||||
: {}),
|
||||
boundaries: [
|
||||
{
|
||||
id: 'critical',
|
||||
description: 'Critical boundary.',
|
||||
include: [`${id}/src/**/*.ts`],
|
||||
thresholds: { lines: 50, branches: 40, functions: 50, statements: 50 },
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
const valid = {
|
||||
policy: {
|
||||
schemaVersion: 'critical-path-coverage/v1',
|
||||
longTermTarget: { lines: 80, branches: 80, functions: 80, statements: 80 },
|
||||
packages,
|
||||
},
|
||||
packageJson: {
|
||||
scripts: { 'test:coverage': 'node scripts/run-coverage.mjs' },
|
||||
devDependencies: { '@vitest/coverage-v8': '^4.1.11' },
|
||||
},
|
||||
workflow:
|
||||
'jobs:\n critical-path-coverage:\n fetch-depth: 0\n needs.select-tests.outputs.coverage_packages\n needs.select-tests.outputs.base_sha\n pnpm test:coverage --packages\n COVERAGE_BASE_REF:\n if: always() && needs.select-tests.outputs.coverage_packages\n actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a\n build:\n runs-on: ubuntu-latest',
|
||||
configs: Object.fromEntries(
|
||||
['server', 'web', 'cli', 'mcp', 'desktop'].map((id) => [
|
||||
id,
|
||||
[
|
||||
"provider: 'v8'",
|
||||
'all: true',
|
||||
"'src/**/*.d.ts'",
|
||||
"'src/**/__fixtures__/**'",
|
||||
"'src/**/fixtures/**'",
|
||||
"'src/**/generated/**'",
|
||||
"'src/**/*.generated.*'",
|
||||
"'src/**/types.ts'",
|
||||
"'src/types/**/*.ts'",
|
||||
].join('\n'),
|
||||
])
|
||||
),
|
||||
};
|
||||
|
||||
test('accepts a complete measured policy and rejects zero floors', () => {
|
||||
assert.deepEqual(validateCoveragePolicy(valid), []);
|
||||
|
||||
const invalid = globalThis.structuredClone(valid);
|
||||
invalid.policy.packages[0].boundaries[0].thresholds.lines = 0;
|
||||
assert.match(validateCoveragePolicy(invalid).join('\n'), /lines threshold/);
|
||||
});
|
||||
|
||||
test('requires the command, CI artifact, and all workspace configs', () => {
|
||||
const invalid = globalThis.structuredClone(valid);
|
||||
invalid.packageJson.scripts = {};
|
||||
invalid.workflow = '';
|
||||
invalid.configs.web = '';
|
||||
const errors = validateCoveragePolicy(invalid).join('\n');
|
||||
|
||||
assert.match(errors, /documented test:coverage command/);
|
||||
assert.match(errors, /CI must define/);
|
||||
assert.match(errors, /fetch the base commit/);
|
||||
assert.match(errors, /web coverage must use V8/);
|
||||
});
|
||||
|
||||
test('requires the coverage job itself to fetch the comparison base', () => {
|
||||
const invalid = globalThis.structuredClone(valid);
|
||||
invalid.workflow = invalid.workflow.replace(' fetch-depth: 0\n', '');
|
||||
|
||||
assert.match(validateCoveragePolicy(invalid).join('\n'), /fetch the base commit/);
|
||||
});
|
||||
|
||||
test('rejects missing governed test inventory entries', () => {
|
||||
const errors = validateCoveragePolicy({
|
||||
...valid,
|
||||
fileExists: (relativePath) => relativePath !== 'server/src/__tests__/critical.test.ts',
|
||||
}).join('\n');
|
||||
|
||||
assert.match(errors, /governed coverage test file does not exist/);
|
||||
});
|
||||
|
||||
test('rejects lowered floors, removed boundaries, and narrowed governed scope', () => {
|
||||
const baseline = globalThis.structuredClone(valid.policy);
|
||||
const lowered = globalThis.structuredClone(valid.policy);
|
||||
lowered.packages[0].report = 'coverage/fabricated/coverage-summary.json';
|
||||
lowered.packages[0].runner.testFiles = [];
|
||||
lowered.packages[0].boundaries[0].thresholds.lines = 49;
|
||||
lowered.packages[0].boundaries[0].include = [];
|
||||
lowered.packages[1].boundaries = [];
|
||||
|
||||
const errors = compareCoveragePolicy(lowered, baseline).join('\n');
|
||||
assert.match(errors, /server\/critical lines threshold cannot decrease/);
|
||||
assert.match(errors, /cannot remove governed runner testFiles input/);
|
||||
assert.match(errors, /cannot remove governed include pattern/);
|
||||
assert.match(errors, /coverage boundary web\/critical cannot be removed/);
|
||||
|
||||
assert.match(
|
||||
validateCoveragePolicy({ ...valid, policy: lowered, baselinePolicy: baseline }).join('\n'),
|
||||
/canonical machine-readable coverage summary/
|
||||
);
|
||||
});
|
||||
|
||||
test('requires exact, tracked, short-lived coverage exceptions', () => {
|
||||
const invalid = globalThis.structuredClone(valid);
|
||||
invalid.policy.packages[0].boundaries[0].exceptions = [
|
||||
{
|
||||
path: 'server/src/**',
|
||||
reason: 'short',
|
||||
owner: 'not a login!',
|
||||
trackingIssue: 'later',
|
||||
reviewBy: 'never',
|
||||
},
|
||||
];
|
||||
const errors = validateCoveragePolicy({ ...invalid, today: '2026-08-23' }).join('\n');
|
||||
|
||||
assert.match(errors, /one exact repository-relative file/);
|
||||
assert.match(errors, /at least 20 characters/);
|
||||
assert.match(errors, /GitHub login/);
|
||||
assert.match(errors, /tracking issue/);
|
||||
assert.match(errors, /real YYYY-MM-DD date/);
|
||||
});
|
||||
538
scripts/check-coverage-ratchets.mjs
Normal file
538
scripts/check-coverage-ratchets.mjs
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
#!/usr/bin/env node
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import path, { matchesGlob } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript';
|
||||
|
||||
import { coverageExceptionErrors } from './coverage-policy-utils.mjs';
|
||||
|
||||
const METRICS = ['lines', 'branches', 'functions', 'statements'];
|
||||
const POLICY_PATH = 'docs/testing/critical-path-coverage.json';
|
||||
const COMMIT_SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
|
||||
|
||||
function roundPercent(covered, total) {
|
||||
if (total === 0) return 100;
|
||||
return Math.floor((covered / total) * 10000) / 100;
|
||||
}
|
||||
|
||||
export function parseSelectedPackages(args, available) {
|
||||
const valueIndex = args.findIndex((arg) => arg === '--packages');
|
||||
const inline = args.find((arg) => arg.startsWith('--packages='));
|
||||
const raw =
|
||||
inline?.slice('--packages='.length) ?? (valueIndex >= 0 ? args[valueIndex + 1] : undefined);
|
||||
const selected = raw
|
||||
? [
|
||||
...new Set(
|
||||
raw
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
),
|
||||
]
|
||||
: [...available];
|
||||
const unknown = selected.filter((id) => !available.includes(id));
|
||||
|
||||
if (unknown.length > 0) throw new Error(`Unknown coverage package(s): ${unknown.join(', ')}`);
|
||||
if (selected.length === 0) throw new Error('At least one coverage package is required.');
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function normalizeCoverageSummary(summary, repoRoot) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(summary)
|
||||
.filter(([file]) => file !== 'total')
|
||||
.map(([file, metrics]) => {
|
||||
const relative = path.isAbsolute(file) ? path.relative(repoRoot, file) : file;
|
||||
return [relative.split(path.sep).join('/'), metrics];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeDetailedCoverage(coverage, repoRoot) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(coverage).map(([file, details]) => {
|
||||
const relative = path.isAbsolute(file) ? path.relative(repoRoot, file) : file;
|
||||
return [relative.split(path.sep).join('/'), details];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function validateException(exception, label, today) {
|
||||
const errors = coverageExceptionErrors(exception, label, today);
|
||||
if (errors.length > 0) throw new Error(errors.join('\n'));
|
||||
}
|
||||
|
||||
function aggregate(entries) {
|
||||
return Object.fromEntries(
|
||||
METRICS.map((metric) => {
|
||||
const total = entries.reduce((sum, [, value]) => sum + value[metric].total, 0);
|
||||
const covered = entries.reduce((sum, [, value]) => sum + value[metric].covered, 0);
|
||||
return [metric, { total, covered, pct: roundPercent(covered, total) }];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function emptyMetrics() {
|
||||
return Object.fromEntries(METRICS.map((metric) => [metric, { total: 0, covered: 0, pct: 0 }]));
|
||||
}
|
||||
|
||||
function uncoveredChangedStatements(details, changedLines) {
|
||||
if (!details || changedLines.size === 0) return [];
|
||||
return Object.entries(details.statementMap ?? {})
|
||||
.filter(([, location]) => {
|
||||
for (let line = location.start.line; line <= location.end.line; line += 1) {
|
||||
if (changedLines.has(line)) return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.filter(([id]) => (details.s?.[id] ?? 0) === 0)
|
||||
.map(([, location]) => location.start.line)
|
||||
.sort((left, right) => left - right);
|
||||
}
|
||||
|
||||
function changedTokenSpans(changedLines, changedSpans, sourceFile) {
|
||||
const lineStarts = sourceFile.getLineStarts();
|
||||
return new Map(
|
||||
[...changedLines].flatMap((line) => {
|
||||
if (line < 1 || line > lineStarts.length) return [];
|
||||
const lineStart = lineStarts[line - 1];
|
||||
const nextLineStart = lineStarts[line] ?? sourceFile.text.length;
|
||||
const lineLength = sourceFile.text
|
||||
.slice(lineStart, nextLineStart)
|
||||
.replace(/[\r\n]+$/, '').length;
|
||||
const spans = changedSpans.get(line) ?? [{ start: 0, end: Number.POSITIVE_INFINITY }];
|
||||
return [
|
||||
[
|
||||
line,
|
||||
spans.map(({ start, end, replacement = '', group }) => ({
|
||||
start: lineStart + Math.min(start, lineLength),
|
||||
end:
|
||||
end === Number.POSITIVE_INFINITY
|
||||
? Number.POSITIVE_INFINITY
|
||||
: lineStart + Math.min(end, lineLength),
|
||||
replacement,
|
||||
group,
|
||||
})),
|
||||
],
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function transpiledRuntime(source, fileName) {
|
||||
const result = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
jsx: ts.JsxEmit.Preserve,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
removeComments: true,
|
||||
target: ts.ScriptTarget.ESNext,
|
||||
},
|
||||
fileName,
|
||||
reportDiagnostics: true,
|
||||
});
|
||||
const hasErrors = (result.diagnostics ?? []).some(
|
||||
({ category }) => category === ts.DiagnosticCategory.Error
|
||||
);
|
||||
return hasErrors ? undefined : result.outputText.trim();
|
||||
}
|
||||
|
||||
export function executableChangedLineNumbers(
|
||||
source,
|
||||
changedLines,
|
||||
previousSource = undefined,
|
||||
changedSpans = new Map(),
|
||||
fileName = 'coverage-source.ts'
|
||||
) {
|
||||
if (changedLines.size === 0) return changedLines;
|
||||
const emitted = transpiledRuntime(source, fileName);
|
||||
if (previousSource !== undefined) {
|
||||
const previousEmitted = transpiledRuntime(previousSource, fileName);
|
||||
if (emitted !== undefined && emitted === previousEmitted) return new Set();
|
||||
}
|
||||
|
||||
const executableLines = new Set();
|
||||
const sourceFile = ts.createSourceFile(
|
||||
fileName,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
false,
|
||||
ts.getScriptKindFromFileName(fileName)
|
||||
);
|
||||
const absoluteSpans = changedTokenSpans(changedLines, changedSpans, sourceFile);
|
||||
const spansByLine = new Map([...absoluteSpans].map(([line]) => [line, []]));
|
||||
const spanGroups = new Map();
|
||||
for (const [line, spans] of absoluteSpans) {
|
||||
for (const span of spans) {
|
||||
const key = span.group ?? Symbol('changed-span');
|
||||
const group = spanGroups.get(key) ?? [];
|
||||
group.push({ ...span, line });
|
||||
spanGroups.set(key, group);
|
||||
}
|
||||
}
|
||||
const revertedRuntime = (spans) => {
|
||||
if (emitted === undefined || spans.some(({ end }) => end === Number.POSITIVE_INFINITY)) {
|
||||
return undefined;
|
||||
}
|
||||
let reverted = source;
|
||||
for (const span of [...spans].sort((left, right) => right.start - left.start)) {
|
||||
reverted = `${reverted.slice(0, span.start)}${span.replacement}${reverted.slice(span.end)}`;
|
||||
}
|
||||
return transpiledRuntime(reverted, fileName);
|
||||
};
|
||||
for (const spans of spanGroups.values()) {
|
||||
const previousRuntime = revertedRuntime(spans);
|
||||
if (previousRuntime !== undefined && previousRuntime === emitted) continue;
|
||||
let selected = spans;
|
||||
if (previousRuntime !== undefined && spans.length > 1) {
|
||||
let candidatesEvaluated = 0;
|
||||
const candidateLimit = 256;
|
||||
for (let size = 1; size <= spans.length && candidatesEvaluated < candidateLimit; size += 1) {
|
||||
const matches = [];
|
||||
const visit = (start, candidate) => {
|
||||
if (candidatesEvaluated >= candidateLimit) return;
|
||||
if (candidate.length === size) {
|
||||
candidatesEvaluated += 1;
|
||||
if (revertedRuntime(candidate) === previousRuntime) matches.push([...candidate]);
|
||||
return;
|
||||
}
|
||||
for (let index = start; index < spans.length; index += 1) {
|
||||
candidate.push(spans[index]);
|
||||
visit(index + 1, candidate);
|
||||
candidate.pop();
|
||||
}
|
||||
};
|
||||
visit(0, []);
|
||||
if (matches.length > 0) {
|
||||
selected = [...new Set(matches.flat())];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const span of selected) spansByLine.get(span.line)?.push(span);
|
||||
}
|
||||
for (const [line, spans] of spansByLine) {
|
||||
if (spans.length > 0) executableLines.add(line);
|
||||
}
|
||||
return executableLines;
|
||||
}
|
||||
|
||||
export function evaluateCoverage(
|
||||
policy,
|
||||
summaries,
|
||||
today = new Date().toISOString().slice(0, 10),
|
||||
changedFiles = [],
|
||||
detailedCoverage = {},
|
||||
changedLineNumbers = new Map()
|
||||
) {
|
||||
if (policy.schemaVersion !== 'critical-path-coverage/v1') {
|
||||
throw new Error(`Unsupported coverage policy schema: ${policy.schemaVersion}`);
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const failures = [];
|
||||
|
||||
for (const packagePolicy of policy.packages) {
|
||||
const summary = summaries[packagePolicy.id];
|
||||
if (!summary) continue;
|
||||
const details = detailedCoverage[packagePolicy.id] ?? {};
|
||||
|
||||
for (const boundary of packagePolicy.boundaries) {
|
||||
const failureCountBeforeBoundary = failures.length;
|
||||
const exceptions = boundary.exceptions ?? [];
|
||||
for (const exception of exceptions) {
|
||||
validateException(exception, `${packagePolicy.id}/${boundary.id}`, today);
|
||||
}
|
||||
|
||||
const entries = Object.entries(summary).filter(([file]) => {
|
||||
const included = boundary.include.some((pattern) => matchesGlob(file, pattern));
|
||||
const excepted = exceptions.some((exception) => file === exception.path);
|
||||
return included && !excepted;
|
||||
});
|
||||
|
||||
if (entries.length === 0) {
|
||||
failures.push(`${packagePolicy.id}/${boundary.id} matched no reported source files`);
|
||||
results.push({
|
||||
package: packagePolicy.id,
|
||||
boundary: boundary.id,
|
||||
description: boundary.description,
|
||||
files: [],
|
||||
metrics: emptyMetrics(),
|
||||
thresholds: boundary.thresholds,
|
||||
status: 'fail',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const metrics = aggregate(entries);
|
||||
const regressions = METRICS.filter(
|
||||
(metric) => metrics[metric].pct < boundary.thresholds[metric]
|
||||
);
|
||||
for (const metric of regressions) {
|
||||
failures.push(
|
||||
`${packagePolicy.id}/${boundary.id} ${metric} ${metrics[metric].pct}% is below ${boundary.thresholds[metric]}%`
|
||||
);
|
||||
}
|
||||
|
||||
for (const changedFile of changedFiles) {
|
||||
const included = boundary.include.some((pattern) => matchesGlob(changedFile, pattern));
|
||||
const excepted = exceptions.some((exception) => exception.path === changedFile);
|
||||
if (!included || excepted) continue;
|
||||
const fileCoverage = summary[changedFile];
|
||||
if (!fileCoverage || fileCoverage.lines.total === 0) {
|
||||
failures.push(
|
||||
`${packagePolicy.id}/${boundary.id} changed critical file ${changedFile} has no executable coverage entry`
|
||||
);
|
||||
} else if (fileCoverage.lines.covered === 0) {
|
||||
failures.push(
|
||||
`${packagePolicy.id}/${boundary.id} changed critical file ${changedFile} has no covered lines`
|
||||
);
|
||||
}
|
||||
const changedLines = changedLineNumbers.get(changedFile) ?? new Set();
|
||||
const detailedFileCoverage = details[changedFile];
|
||||
if (changedLines.size > 0 && !detailedFileCoverage) {
|
||||
failures.push(
|
||||
`${packagePolicy.id}/${boundary.id} changed critical file ${changedFile} has no statement coverage entry`
|
||||
);
|
||||
} else {
|
||||
const uncoveredLines = uncoveredChangedStatements(detailedFileCoverage, changedLines);
|
||||
if (uncoveredLines.length > 0) {
|
||||
failures.push(
|
||||
`${packagePolicy.id}/${boundary.id} changed critical file ${changedFile} has uncovered executable statements on line(s) ${uncoveredLines.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.push({
|
||||
package: packagePolicy.id,
|
||||
boundary: boundary.id,
|
||||
description: boundary.description,
|
||||
files: entries.map(([file]) => file).sort(),
|
||||
metrics,
|
||||
thresholds: boundary.thresholds,
|
||||
status: failures.length === failureCountBeforeBoundary ? 'pass' : 'fail',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { results, failures };
|
||||
}
|
||||
|
||||
export function changedFilesFromBase(repoRoot, baselineRef) {
|
||||
if (!baselineRef) return [];
|
||||
if (!COMMIT_SHA_PATTERN.test(baselineRef)) {
|
||||
throw new Error('COVERAGE_BASE_REF must be a hexadecimal commit ID.');
|
||||
}
|
||||
return execFileSync(
|
||||
'git',
|
||||
['diff', '--name-only', '--diff-filter=ACMR', `${baselineRef}...HEAD`],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
}
|
||||
)
|
||||
.split('\n')
|
||||
.map((file) => file.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function changedLineNumbersFromBase(repoRoot, baselineRef, changedFiles) {
|
||||
if (!baselineRef || changedFiles.length === 0) return new Map();
|
||||
const changedLines = new Map();
|
||||
for (const file of changedFiles) {
|
||||
const diff = execFileSync(
|
||||
'git',
|
||||
['diff', '--unified=0', '--no-color', `${baselineRef}...HEAD`, '--', file],
|
||||
{ cwd: repoRoot, encoding: 'utf8' }
|
||||
);
|
||||
const parsedLines = parseChangedLineNumbers(diff);
|
||||
const parsedSpans = parseChangedLineSpans(diff);
|
||||
if (!/\.[cm]?[jt]sx?$/.test(file)) {
|
||||
changedLines.set(file, parsedLines);
|
||||
continue;
|
||||
}
|
||||
const source = readFileSync(path.join(repoRoot, file), 'utf8');
|
||||
let previousSource;
|
||||
try {
|
||||
previousSource = execFileSync('git', ['show', `${baselineRef}:${file}`], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
} catch {
|
||||
previousSource = '';
|
||||
}
|
||||
changedLines.set(
|
||||
file,
|
||||
executableChangedLineNumbers(source, parsedLines, previousSource, parsedSpans, file)
|
||||
);
|
||||
}
|
||||
return changedLines;
|
||||
}
|
||||
|
||||
export function parseChangedLineNumbers(diff) {
|
||||
const lines = new Set();
|
||||
for (const match of diff.matchAll(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm)) {
|
||||
const start = Number(match[1]);
|
||||
const count = match[2] === undefined ? 1 : Number(match[2]);
|
||||
for (let line = start; line < start + count; line += 1) lines.add(line);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function parseChangedLineSpans(diff) {
|
||||
const spans = new Map();
|
||||
const diffLines = diff.split('\n');
|
||||
for (let index = 0; index < diffLines.length; index += 1) {
|
||||
const header = diffLines[index].match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
||||
if (!header) continue;
|
||||
const hunkGroup = `hunk-${index}`;
|
||||
let newLine = Number(header[3]);
|
||||
const removed = [];
|
||||
const added = [];
|
||||
for (index += 1; index < diffLines.length && !diffLines[index].startsWith('@@ '); index += 1) {
|
||||
const line = diffLines[index];
|
||||
if (line.startsWith('-')) removed.push(line.slice(1));
|
||||
if (line.startsWith('+')) {
|
||||
added.push({ line: newLine, text: line.slice(1) });
|
||||
newLine += 1;
|
||||
}
|
||||
}
|
||||
index -= 1;
|
||||
for (let addedIndex = 0; addedIndex < added.length; addedIndex += 1) {
|
||||
const current = added[addedIndex];
|
||||
const previous = removed.length === added.length ? removed[addedIndex] : undefined;
|
||||
if (previous === undefined) {
|
||||
spans.set(current.line, [
|
||||
{
|
||||
start: 0,
|
||||
end: current.text.length,
|
||||
replacement: addedIndex === 0 ? removed.join('\n') : '',
|
||||
group: hunkGroup,
|
||||
},
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
let start = 0;
|
||||
while (
|
||||
start < previous.length &&
|
||||
start < current.text.length &&
|
||||
previous[start] === current.text[start]
|
||||
) {
|
||||
start += 1;
|
||||
}
|
||||
let previousEnd = previous.length;
|
||||
let currentEnd = current.text.length;
|
||||
while (
|
||||
previousEnd > start &&
|
||||
currentEnd > start &&
|
||||
previous[previousEnd - 1] === current.text[currentEnd - 1]
|
||||
) {
|
||||
previousEnd -= 1;
|
||||
currentEnd -= 1;
|
||||
}
|
||||
spans.set(current.line, [
|
||||
{
|
||||
start,
|
||||
end: currentEnd,
|
||||
replacement: previous.slice(start, previousEnd),
|
||||
group: hunkGroup,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
export function markdownSummary(evaluation, longTermTarget) {
|
||||
const lines = [
|
||||
'### Critical-path coverage ratchets',
|
||||
'',
|
||||
'| Package | Boundary | Lines | Branches | Functions | Statements | Result |',
|
||||
'| --- | --- | ---: | ---: | ---: | ---: | --- |',
|
||||
];
|
||||
|
||||
for (const result of evaluation.results) {
|
||||
lines.push(
|
||||
`| ${result.package} | ${result.boundary} | ${result.metrics.lines.pct}% | ${result.metrics.branches.pct}% | ${result.metrics.functions.pct}% | ${result.metrics.statements.pct}% | ${result.status} |`
|
||||
);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
`Long-term critical-path target: ${longTermTarget.lines}% lines, ${longTermTarget.branches}% branches, ${longTermTarget.functions}% functions, and ${longTermTarget.statements}% statements.`,
|
||||
''
|
||||
);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export async function runCoverageRatchets({
|
||||
repoRoot = process.cwd(),
|
||||
args = process.argv.slice(2),
|
||||
} = {}) {
|
||||
const policy = JSON.parse(await readFile(path.join(repoRoot, POLICY_PATH), 'utf8'));
|
||||
const available = policy.packages.map(({ id }) => id);
|
||||
const selected = parseSelectedPackages(args, available);
|
||||
const summaries = {};
|
||||
const detailedCoverage = {};
|
||||
|
||||
for (const packagePolicy of policy.packages) {
|
||||
if (!selected.includes(packagePolicy.id)) continue;
|
||||
const reportPath = path.join(repoRoot, packagePolicy.report);
|
||||
const [summary, details] = await Promise.all([
|
||||
readFile(reportPath, 'utf8').then(JSON.parse),
|
||||
readFile(path.join(path.dirname(reportPath), 'coverage-final.json'), 'utf8').then(JSON.parse),
|
||||
]);
|
||||
summaries[packagePolicy.id] = normalizeCoverageSummary(summary, repoRoot);
|
||||
detailedCoverage[packagePolicy.id] = normalizeDetailedCoverage(details, repoRoot);
|
||||
}
|
||||
|
||||
const changedFiles = changedFilesFromBase(repoRoot, process.env.COVERAGE_BASE_REF);
|
||||
const changedLineNumbers = changedLineNumbersFromBase(
|
||||
repoRoot,
|
||||
process.env.COVERAGE_BASE_REF,
|
||||
changedFiles
|
||||
);
|
||||
const evaluation = evaluateCoverage(
|
||||
policy,
|
||||
summaries,
|
||||
undefined,
|
||||
changedFiles,
|
||||
detailedCoverage,
|
||||
changedLineNumbers
|
||||
);
|
||||
const machineReport = {
|
||||
schemaVersion: 'critical-path-coverage-report/v1',
|
||||
longTermTarget: policy.longTermTarget,
|
||||
packages: selected,
|
||||
results: evaluation.results,
|
||||
failures: evaluation.failures,
|
||||
};
|
||||
const markdown = markdownSummary(evaluation, policy.longTermTarget);
|
||||
const outputDir = path.join(repoRoot, 'coverage');
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(outputDir, 'critical-path-summary.json'),
|
||||
`${JSON.stringify(machineReport, null, 2)}\n`
|
||||
);
|
||||
await writeFile(path.join(outputDir, 'critical-path-summary.md'), markdown);
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
await appendFile(process.env.GITHUB_STEP_SUMMARY, markdown);
|
||||
}
|
||||
|
||||
console.log(markdown);
|
||||
if (evaluation.failures.length > 0) {
|
||||
throw new Error(`Coverage ratchet failed:\n- ${evaluation.failures.join('\n- ')}`);
|
||||
}
|
||||
return machineReport;
|
||||
}
|
||||
|
||||
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
||||
if (isMain) {
|
||||
runCoverageRatchets().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
336
scripts/check-coverage-ratchets.test.mjs
Normal file
336
scripts/check-coverage-ratchets.test.mjs
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
evaluateCoverage,
|
||||
executableChangedLineNumbers,
|
||||
markdownSummary,
|
||||
normalizeCoverageSummary,
|
||||
normalizeDetailedCoverage,
|
||||
parseChangedLineNumbers,
|
||||
parseChangedLineSpans,
|
||||
parseSelectedPackages,
|
||||
} from './check-coverage-ratchets.mjs';
|
||||
|
||||
const metric = (covered, total) => ({ covered, total, skipped: 0, pct: (covered / total) * 100 });
|
||||
const fileCoverage = (covered, total) => ({
|
||||
lines: metric(covered, total),
|
||||
branches: metric(covered, total),
|
||||
functions: metric(covered, total),
|
||||
statements: metric(covered, total),
|
||||
});
|
||||
|
||||
const policy = {
|
||||
schemaVersion: 'critical-path-coverage/v1',
|
||||
longTermTarget: { lines: 80, branches: 80, functions: 80, statements: 80 },
|
||||
packages: [
|
||||
{
|
||||
id: 'server',
|
||||
runner: { testFiles: ['src/__tests__/auth.test.ts'] },
|
||||
boundaries: [
|
||||
{
|
||||
id: 'auth',
|
||||
description: 'Authentication boundary.',
|
||||
include: ['server/src/middleware/**/*.ts'],
|
||||
thresholds: { lines: 75, branches: 75, functions: 75, statements: 75 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('normalizes absolute report paths to repository-relative POSIX paths', () => {
|
||||
const summary = {
|
||||
total: fileCoverage(1, 1),
|
||||
'/repo/server/src/middleware/auth.ts': fileCoverage(3, 4),
|
||||
};
|
||||
|
||||
assert.deepEqual(Object.keys(normalizeCoverageSummary(summary, '/repo')), [
|
||||
'server/src/middleware/auth.ts',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
Object.keys(
|
||||
normalizeDetailedCoverage(
|
||||
{ '/repo/server/src/middleware/auth.ts': { statementMap: {}, s: {} } },
|
||||
'/repo'
|
||||
)
|
||||
),
|
||||
['server/src/middleware/auth.ts']
|
||||
);
|
||||
});
|
||||
|
||||
test('extracts added and modified line numbers from zero-context diffs', () => {
|
||||
const lines = parseChangedLineNumbers(
|
||||
'@@ -4 +4,2 @@\n-old\n+new\n+next\n@@ -10,2 +11 @@\n-old\n-old\n+new\n@@ -20 +21,0 @@\n-old\n'
|
||||
);
|
||||
assert.deepEqual([...lines], [4, 5, 11]);
|
||||
});
|
||||
|
||||
test('keeps only executable changed lines and ignores comment-only or type-only edits', () => {
|
||||
const previous = 'const value: OldType = run(\n input\n);\n';
|
||||
const commentOnly = 'const value: OldType = run(\n // rationale\n input\n);\n';
|
||||
assert.deepEqual([...executableChangedLineNumbers(commentOnly, new Set([2]), previous)], []);
|
||||
|
||||
const typeOnly = 'const value: NewType = run(\n input\n);\n';
|
||||
assert.deepEqual([...executableChangedLineNumbers(typeOnly, new Set([1]), previous)], []);
|
||||
|
||||
const runtimeChange = 'const value: OldType = run(\n replacement\n);\n';
|
||||
assert.deepEqual([...executableChangedLineNumbers(runtimeChange, new Set([2]), previous)], [2]);
|
||||
});
|
||||
|
||||
test('ignores type-only tokens when the same file also changes runtime behavior', () => {
|
||||
const previous = 'type Input = OldType;\nconst value = run(input);\n';
|
||||
const current = 'type Input = NewType;\nconst value = run(replacement);\n';
|
||||
const diff = [
|
||||
'@@ -1,2 +1,2 @@',
|
||||
'-type Input = OldType;',
|
||||
'-const value = run(input);',
|
||||
'+type Input = NewType;',
|
||||
'+const value = run(replacement);',
|
||||
].join('\n');
|
||||
const changedLines = parseChangedLineNumbers(diff);
|
||||
const changedSpans = parseChangedLineSpans(diff);
|
||||
|
||||
assert.deepEqual(
|
||||
[...executableChangedLineNumbers(current, changedLines, previous, changedSpans)],
|
||||
[2]
|
||||
);
|
||||
});
|
||||
|
||||
test('distinguishes added type punctuation from deletion-only runtime edits', () => {
|
||||
const previous = [
|
||||
'const value = run(input);',
|
||||
'const result = keep(original);',
|
||||
'verify(input);',
|
||||
].join('\n');
|
||||
const current = [
|
||||
'const value: Input = run(input);',
|
||||
'const result = keep(replacement);',
|
||||
'verify();',
|
||||
].join('\n');
|
||||
const diff = [
|
||||
'@@ -1,3 +1,3 @@',
|
||||
'-const value = run(input);',
|
||||
'-const result = keep(original);',
|
||||
'-verify(input);',
|
||||
'+const value: Input = run(input);',
|
||||
'+const result = keep(replacement);',
|
||||
'+verify();',
|
||||
].join('\n');
|
||||
const changedLines = parseChangedLineNumbers(diff);
|
||||
const changedSpans = parseChangedLineSpans(diff);
|
||||
|
||||
assert.deepEqual(
|
||||
[...executableChangedLineNumbers(current, changedLines, previous, changedSpans)],
|
||||
[2, 3]
|
||||
);
|
||||
});
|
||||
|
||||
test('retains runtime-significant whitespace insertions', () => {
|
||||
const previous = 'const kind = typeofx;\n';
|
||||
const current = 'const kind = typeof x;\n';
|
||||
const diff = '@@ -1 +1 @@\n-const kind = typeofx;\n+const kind = typeof x;\n';
|
||||
const changedLines = parseChangedLineNumbers(diff);
|
||||
const changedSpans = parseChangedLineSpans(diff);
|
||||
|
||||
assert.deepEqual(
|
||||
[...executableChangedLineNumbers(current, changedLines, previous, changedSpans)],
|
||||
[1]
|
||||
);
|
||||
});
|
||||
|
||||
test('treats line-count-only statement formatting as one runtime-neutral hunk', () => {
|
||||
const previous = ['const value = run(input);', '', 'const result = keep(original);'].join('\n');
|
||||
const current = [
|
||||
'const value = run(',
|
||||
' input',
|
||||
');',
|
||||
'',
|
||||
'const result = keep(replacement);',
|
||||
].join('\n');
|
||||
const diff = [
|
||||
'@@ -1 +1,3 @@',
|
||||
'-const value = run(input);',
|
||||
'+const value = run(',
|
||||
'+ input',
|
||||
'+);',
|
||||
'@@ -3 +5 @@',
|
||||
'-const result = keep(original);',
|
||||
'+const result = keep(replacement);',
|
||||
].join('\n');
|
||||
const changedLines = parseChangedLineNumbers(diff);
|
||||
const changedSpans = parseChangedLineSpans(diff);
|
||||
|
||||
assert.deepEqual(
|
||||
[...executableChangedLineNumbers(current, changedLines, previous, changedSpans)],
|
||||
[5]
|
||||
);
|
||||
});
|
||||
|
||||
test('treats equal-line-count statement formatting as one runtime-neutral hunk', () => {
|
||||
const previous = [
|
||||
'const value = run(input);',
|
||||
'const other = use(input);',
|
||||
'const result = keep(original);',
|
||||
].join('\n');
|
||||
const current = [
|
||||
'const value = run(',
|
||||
'input); const other = use(input);',
|
||||
'const result = keep(replacement);',
|
||||
].join('\n');
|
||||
const diff = [
|
||||
'@@ -1,3 +1,3 @@',
|
||||
'-const value = run(input);',
|
||||
'-const other = use(input);',
|
||||
'-const result = keep(original);',
|
||||
'+const value = run(',
|
||||
'+input); const other = use(input);',
|
||||
'+const result = keep(replacement);',
|
||||
].join('\n');
|
||||
const changedLines = parseChangedLineNumbers(diff);
|
||||
const changedSpans = parseChangedLineSpans(diff);
|
||||
|
||||
assert.deepEqual(
|
||||
[...executableChangedLineNumbers(current, changedLines, previous, changedSpans)],
|
||||
[3]
|
||||
);
|
||||
});
|
||||
|
||||
test('aggregates a boundary and accepts its measured floor', () => {
|
||||
const evaluation = evaluateCoverage(policy, {
|
||||
server: {
|
||||
'server/src/middleware/auth.ts': fileCoverage(3, 4),
|
||||
'server/src/routes/tasks.ts': fileCoverage(0, 10),
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(evaluation.failures, []);
|
||||
assert.equal(evaluation.results[0].metrics.lines.pct, 75);
|
||||
assert.match(markdownSummary(evaluation, policy.longTermTarget), /server \| auth \| 75%/);
|
||||
});
|
||||
|
||||
test('rejects a metric regression and a boundary without report entries', () => {
|
||||
const regression = evaluateCoverage(policy, {
|
||||
server: { 'server/src/middleware/auth.ts': fileCoverage(2, 4) },
|
||||
});
|
||||
assert.equal(regression.failures.length, 4);
|
||||
|
||||
const missing = evaluateCoverage(policy, {
|
||||
server: { 'server/src/routes/tasks.ts': fileCoverage(4, 4) },
|
||||
});
|
||||
assert.deepEqual(missing.failures, ['server/auth matched no reported source files']);
|
||||
assert.equal(missing.results[0].status, 'fail');
|
||||
assert.equal(missing.results[0].metrics.lines.pct, 0);
|
||||
});
|
||||
|
||||
test('requires bounded reviewed exceptions and omits active exceptions', () => {
|
||||
const withException = globalThis.structuredClone(policy);
|
||||
withException.packages[0].boundaries[0].exceptions = [
|
||||
{
|
||||
path: 'server/src/middleware/legacy.ts',
|
||||
reason: 'Legacy adapter awaiting removal.',
|
||||
owner: 'BradGroux',
|
||||
trackingIssue: '#1169',
|
||||
reviewBy: '2026-09-30',
|
||||
},
|
||||
];
|
||||
const evaluation = evaluateCoverage(
|
||||
withException,
|
||||
{
|
||||
server: {
|
||||
'server/src/middleware/auth.ts': fileCoverage(4, 4),
|
||||
'server/src/middleware/legacy.ts': fileCoverage(0, 4),
|
||||
},
|
||||
},
|
||||
'2026-08-23'
|
||||
);
|
||||
assert.deepEqual(evaluation.failures, []);
|
||||
assert.deepEqual(evaluation.results[0].files, ['server/src/middleware/auth.ts']);
|
||||
|
||||
withException.packages[0].boundaries[0].exceptions[0].reviewBy = 'never';
|
||||
assert.throws(
|
||||
() => evaluateCoverage(withException, { server: {} }, '2026-08-23'),
|
||||
/reviewBy must be a real YYYY-MM-DD date/
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects a changed critical file without covered executable lines', () => {
|
||||
const uncovered = evaluateCoverage(
|
||||
policy,
|
||||
{
|
||||
server: {
|
||||
'server/src/middleware/auth.ts': fileCoverage(3, 4),
|
||||
'server/src/middleware/new-auth.ts': fileCoverage(0, 4),
|
||||
},
|
||||
},
|
||||
'2026-08-23',
|
||||
['server/src/middleware/new-auth.ts']
|
||||
);
|
||||
assert.ok(
|
||||
uncovered.failures.includes(
|
||||
'server/auth changed critical file server/src/middleware/new-auth.ts has no covered lines'
|
||||
)
|
||||
);
|
||||
assert.equal(uncovered.results[0].status, 'fail');
|
||||
});
|
||||
|
||||
test('requires changed executable statements themselves to be covered', () => {
|
||||
const summary = { server: { 'server/src/middleware/auth.ts': fileCoverage(3, 4) } };
|
||||
const details = {
|
||||
server: {
|
||||
'server/src/middleware/auth.ts': {
|
||||
statementMap: {
|
||||
0: { start: { line: 10 }, end: { line: 10 } },
|
||||
1: { start: { line: 20 }, end: { line: 22 } },
|
||||
},
|
||||
s: { 0: 1, 1: 0 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const changedLines = new Map([['server/src/middleware/auth.ts', new Set([21])]]);
|
||||
const uncovered = evaluateCoverage(
|
||||
policy,
|
||||
summary,
|
||||
'2026-08-23',
|
||||
['server/src/middleware/auth.ts'],
|
||||
details,
|
||||
changedLines
|
||||
);
|
||||
assert.match(uncovered.failures.join('\n'), /uncovered executable statements on line\(s\) 20/);
|
||||
assert.equal(uncovered.results[0].status, 'fail');
|
||||
|
||||
details.server['server/src/middleware/auth.ts'].s[1] = 1;
|
||||
const covered = evaluateCoverage(
|
||||
policy,
|
||||
summary,
|
||||
'2026-08-23',
|
||||
['server/src/middleware/auth.ts'],
|
||||
details,
|
||||
changedLines
|
||||
);
|
||||
assert.deepEqual(covered.failures, []);
|
||||
assert.equal(covered.results[0].status, 'pass');
|
||||
});
|
||||
|
||||
test('rejects broad and untracked exceptions', () => {
|
||||
const invalid = globalThis.structuredClone(policy);
|
||||
invalid.packages[0].boundaries[0].exceptions = [
|
||||
{
|
||||
path: 'server/src/middleware/**',
|
||||
reason: 'This reason is long enough for review.',
|
||||
owner: 'BradGroux',
|
||||
trackingIssue: '#1169',
|
||||
reviewBy: '2026-09-01',
|
||||
},
|
||||
];
|
||||
assert.throws(
|
||||
() => evaluateCoverage(invalid, { server: {} }, '2026-08-23'),
|
||||
/one exact repository-relative file/
|
||||
);
|
||||
});
|
||||
|
||||
test('validates selected coverage packages', () => {
|
||||
assert.deepEqual(parseSelectedPackages([], ['server', 'web']), ['server', 'web']);
|
||||
assert.deepEqual(parseSelectedPackages(['--packages=web'], ['server', 'web']), ['web']);
|
||||
assert.throws(() => parseSelectedPackages(['--packages', 'unknown'], ['server']), /Unknown/);
|
||||
});
|
||||
|
|
@ -14,8 +14,9 @@ export const CADENCE_CONTRACTS = {
|
|||
pattern: /one independently shippable behavior/i,
|
||||
},
|
||||
{
|
||||
description: 'narrowest useful implementation loop',
|
||||
pattern: /run the narrowest useful loop/i,
|
||||
description: 'ordinary implementation defers workspace tests',
|
||||
pattern:
|
||||
/Do not run workspace unit, coverage, E2E, desktop packaging, or Docker contract tests between implementation PRs/i,
|
||||
},
|
||||
{
|
||||
description: '45-minute split or escalate delivery checkpoint',
|
||||
|
|
@ -62,7 +63,8 @@ export const CADENCE_CONTRACTS = {
|
|||
},
|
||||
{
|
||||
description: 'affected-boundary runtime smoke tests',
|
||||
pattern: /Run browser or API smoke tests only when the change affects/i,
|
||||
pattern:
|
||||
/Run browser or API smoke tests only at an explicit integration or release milestone/i,
|
||||
},
|
||||
],
|
||||
'.github/PULL_REQUEST_TEMPLATE.md': [
|
||||
|
|
@ -75,8 +77,8 @@ export const CADENCE_CONTRACTS = {
|
|||
pattern: /\*\*Verification tier:\*\*/i,
|
||||
},
|
||||
{
|
||||
description: 'focused changed-package test tier',
|
||||
pattern: /Focused changed-package tests/i,
|
||||
description: 'explicit focused diagnostic tier',
|
||||
pattern: /Explicit focused diagnostic/i,
|
||||
},
|
||||
{
|
||||
description: 'full milestone gate tier',
|
||||
|
|
@ -271,8 +273,7 @@ export function findUnsafeCanonicalCadenceStatements(files) {
|
|||
|
||||
export function findAmbiguousFocusedTestCommands(files) {
|
||||
const violations = [];
|
||||
const pattern =
|
||||
/\bpnpm\s+(?:--filter(?:=|\s+)|-F\s+)\S+\s+(?:run\s+)?test\s+--(?=\s)/gi;
|
||||
const pattern = /\bpnpm\s+(?:--filter(?:=|\s+)|-F\s+)\S+\s+(?:run\s+)?test\s+--(?=\s)/gi;
|
||||
|
||||
for (const [file, content] of Object.entries(files)) {
|
||||
const normalized = normalizeWhitespace(content);
|
||||
|
|
|
|||
214
scripts/check-docker-image.mjs
Normal file
214
scripts/check-docker-image.mjs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const image = process.env.VERITAS_DOCKER_IMAGE || process.argv[2] || 'veritas-kanban:contract';
|
||||
const configuredMaxBytes = process.env.VERITAS_DOCKER_MAX_BYTES;
|
||||
const defaultMaxBytesByArchitecture = {
|
||||
arm64: 200_000_000,
|
||||
amd64: 600_000_000,
|
||||
};
|
||||
const containerName = `veritas-kanban-contract-${process.pid}`;
|
||||
const volumeName = `${containerName}-data`;
|
||||
const adminKey = randomBytes(24).toString('hex');
|
||||
const expectedVersion = JSON.parse(
|
||||
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
||||
).version;
|
||||
|
||||
function run(args) {
|
||||
const result = spawnSync('docker', args, { encoding: 'utf8', stdio: 'pipe' });
|
||||
if (result.status !== 0) {
|
||||
const detail = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
||||
throw new Error(detail || `Docker command failed with status ${result.status}`);
|
||||
}
|
||||
return result.stdout?.trim() ?? '';
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function containerLogs() {
|
||||
const result = spawnSync('docker', ['logs', containerName], {
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
return [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
||||
}
|
||||
|
||||
async function waitForHealthyContainer() {
|
||||
const deadline = Date.now() + 90_000;
|
||||
while (Date.now() < deadline) {
|
||||
const state = JSON.parse(
|
||||
run(['inspect', '--format', '{{json .State}}', containerName])
|
||||
);
|
||||
if (state.Health?.Status === 'healthy') return;
|
||||
if (state.Status === 'exited' || state.Status === 'dead') {
|
||||
throw new Error(
|
||||
`Container stopped before becoming healthy (${state.Status})\n${containerLogs()}`
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => globalThis.setTimeout(resolve, 1_000));
|
||||
}
|
||||
const health = run(['inspect', '--format', '{{json .State.Health}}', containerName]);
|
||||
throw new Error(
|
||||
`Container did not become healthy within 90 seconds\nHealth: ${health}\n${containerLogs()}`
|
||||
);
|
||||
}
|
||||
|
||||
const runtimeProbe = String.raw`
|
||||
import bcrypt from 'bcrypt';
|
||||
import { access } from 'node:fs/promises';
|
||||
|
||||
const assert = (condition, message) => {
|
||||
if (!condition) throw new Error(message);
|
||||
};
|
||||
const response = async (path, init) => fetch('http://127.0.0.1:3001' + path, init);
|
||||
|
||||
const health = await response('/health');
|
||||
assert(health.status === 200, 'GET /health did not return 200');
|
||||
|
||||
const readiness = await response('/health/ready');
|
||||
const readinessBody = await readiness.json();
|
||||
assert(readiness.status === 200, 'GET /health/ready did not return 200');
|
||||
assert(readinessBody.checks?.sqlite === 'ok', 'SQLite readiness was not healthy');
|
||||
|
||||
const index = await response('/');
|
||||
const html = await index.text();
|
||||
assert(index.status === 200 && html.includes('id="root"'), 'Built web app was not served');
|
||||
|
||||
const unauthenticated = await response('/api/tasks');
|
||||
assert(unauthenticated.status === 401, 'Protected API did not reject an unauthenticated request');
|
||||
|
||||
const authenticated = await response('/api/tasks', {
|
||||
headers: { 'X-API-Key': process.env.VERITAS_ADMIN_KEY },
|
||||
});
|
||||
assert(authenticated.status === 200, 'Admin API key did not authenticate');
|
||||
|
||||
const deepHealth = await response('/api/health/deep', {
|
||||
headers: { 'X-API-Key': process.env.VERITAS_ADMIN_KEY },
|
||||
});
|
||||
const deepHealthBody = await deepHealth.json();
|
||||
assert(deepHealth.status === 200, 'Deep health endpoint did not return 200');
|
||||
assert(deepHealthBody.status === 'ok', 'Deep health reported a degraded runtime');
|
||||
assert(
|
||||
deepHealthBody.version === ${JSON.stringify(expectedVersion)},
|
||||
'Deep health did not report release version ${expectedVersion}'
|
||||
);
|
||||
assert(deepHealthBody.checks?.storage === 'ok', 'Storage integrity check was not healthy');
|
||||
assert(deepHealthBody.sqlite?.healthPosture === 'healthy', 'SQLite startup was not healthy');
|
||||
assert(
|
||||
deepHealthBody.dataDirectory?.path === '/app/data/.veritas-kanban',
|
||||
'Runtime state did not resolve beneath the mounted DATA_DIR'
|
||||
);
|
||||
|
||||
const backup = await response('/api/v1/sqlite/export', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Key': process.env.VERITAS_ADMIN_KEY,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sqlitePath: '/app/data/.veritas-kanban/veritas.db',
|
||||
outputDir: '/app/data/backups/docker-contract',
|
||||
}),
|
||||
});
|
||||
const backupBody = await backup.json();
|
||||
assert(backup.status === 200, 'SQLite backup export did not return 200');
|
||||
assert(
|
||||
backupBody.success === true &&
|
||||
backupBody.data?.bundlePath === '/app/data/backups/docker-contract',
|
||||
'SQLite backup export escaped the mounted DATA_DIR'
|
||||
);
|
||||
await access('/app/data/backups/docker-contract/manifest.json');
|
||||
|
||||
const hash = await bcrypt.hash('native-module-probe', 4);
|
||||
assert(await bcrypt.compare('native-module-probe', hash), 'bcrypt native module failed');
|
||||
`;
|
||||
|
||||
let started = false;
|
||||
let volumeCreated = false;
|
||||
try {
|
||||
const architecture = run(['image', 'inspect', image, '--format', '{{.Architecture}}']);
|
||||
const maxBytes = configuredMaxBytes
|
||||
? Number(configuredMaxBytes)
|
||||
: defaultMaxBytesByArchitecture[architecture];
|
||||
assert(
|
||||
maxBytes !== undefined,
|
||||
`No Docker image size budget is defined for architecture ${architecture}; set VERITAS_DOCKER_MAX_BYTES explicitly`
|
||||
);
|
||||
assert(Number.isFinite(maxBytes) && maxBytes > 0, 'VERITAS_DOCKER_MAX_BYTES must be positive');
|
||||
|
||||
const imageBytes = Number(run(['image', 'inspect', image, '--format', '{{.Size}}']));
|
||||
assert(Number.isFinite(imageBytes), `Could not read image size for ${image}`);
|
||||
if (imageBytes >= maxBytes) {
|
||||
const diagnostics = run([
|
||||
'run',
|
||||
'--rm',
|
||||
'--entrypoint',
|
||||
'sh',
|
||||
image,
|
||||
'-c',
|
||||
'du -ak /app /usr/local 2>/dev/null | sort -nr | head -25',
|
||||
]);
|
||||
throw new Error(
|
||||
`Docker image is ${imageBytes.toLocaleString()} bytes; budget is below ${maxBytes.toLocaleString()} bytes\nLargest runtime paths (KiB):\n${diagnostics}`
|
||||
);
|
||||
}
|
||||
|
||||
const configuredUser = run(['image', 'inspect', image, '--format', '{{.Config.User}}']);
|
||||
assert(configuredUser === 'veritas', `Expected image user veritas, found ${configuredUser || 'root'}`);
|
||||
|
||||
run(['volume', 'create', volumeName]);
|
||||
volumeCreated = true;
|
||||
run([
|
||||
'run',
|
||||
'--detach',
|
||||
'--name',
|
||||
containerName,
|
||||
'--mount',
|
||||
`type=volume,source=${volumeName},target=/app/data`,
|
||||
'--env',
|
||||
`VERITAS_ADMIN_KEY=${adminKey}`,
|
||||
'--env',
|
||||
'VERITAS_STORAGE=sqlite',
|
||||
image,
|
||||
]);
|
||||
started = true;
|
||||
|
||||
await waitForHealthyContainer();
|
||||
|
||||
run([
|
||||
'exec',
|
||||
containerName,
|
||||
'sh',
|
||||
'-c',
|
||||
'test "$(id -u)" = 1001 && test ! -e /app/cli && test ! -e /app/mcp && test ! -e /app/pnpm-lock.yaml && test -f /app/data/.veritas-kanban/veritas.db',
|
||||
]);
|
||||
run(['exec', containerName, 'node', '--input-type=module', '--eval', runtimeProbe]);
|
||||
|
||||
run(['stop', '--time', '15', containerName]);
|
||||
const stoppedState = JSON.parse(
|
||||
run(['inspect', '--format', '{{json .State}}', containerName])
|
||||
);
|
||||
assert(stoppedState.Status === 'exited', 'Container did not stop cleanly');
|
||||
assert(stoppedState.ExitCode === 0, `Container exited with code ${stoppedState.ExitCode}`);
|
||||
run(['rm', containerName]);
|
||||
started = false;
|
||||
|
||||
console.log(
|
||||
`Docker image contract passed on ${architecture}: ${imageBytes.toLocaleString()} bytes (< ${maxBytes.toLocaleString()})`
|
||||
);
|
||||
console.log(
|
||||
'Runtime smoke passed: non-root user, version, mounted paths, SQLite, backup, auth, web assets, health, bcrypt, and clean shutdown'
|
||||
);
|
||||
} finally {
|
||||
if (started) {
|
||||
spawnSync('docker', ['rm', '--force', containerName], { stdio: 'ignore' });
|
||||
}
|
||||
if (volumeCreated) {
|
||||
spawnSync('docker', ['volume', 'rm', volumeName], { stdio: 'ignore' });
|
||||
}
|
||||
}
|
||||
84
scripts/check-gitleaks.mjs
Normal file
84
scripts/check-gitleaks.mjs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
copyFileSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readlinkSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const binary = process.env.GITLEAKS_BIN || 'gitleaks';
|
||||
|
||||
function runGitleaks(args, cwd) {
|
||||
return spawnSync(binary, args, { cwd, encoding: 'utf8', env: process.env });
|
||||
}
|
||||
|
||||
function fail(message, result) {
|
||||
console.error(message);
|
||||
if (result?.error?.code === 'ENOENT') {
|
||||
console.error(`Gitleaks binary not found at ${binary}. Install gitleaks or set GITLEAKS_BIN.`);
|
||||
} else if (result?.stderr) {
|
||||
console.error(result.stderr.trim());
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function stageScannableSource() {
|
||||
const repositoryRoot = process.cwd();
|
||||
const listing = spawnSync(
|
||||
'git',
|
||||
['ls-files', '--cached', '--others', '--exclude-standard', '-z'],
|
||||
{ cwd: repositoryRoot, encoding: 'utf8' }
|
||||
);
|
||||
if (listing.status !== 0) fail('Could not inventory Git-managed source files.', listing);
|
||||
|
||||
const stagingDirectory = mkdtempSync(join(tmpdir(), 'veritas-gitleaks-source-'));
|
||||
for (const relativePath of listing.stdout.split('\0').filter(Boolean)) {
|
||||
const source = resolve(repositoryRoot, relativePath);
|
||||
const destination = resolve(stagingDirectory, relativePath);
|
||||
mkdirSync(dirname(destination), { recursive: true });
|
||||
const stats = lstatSync(source);
|
||||
if (stats.isSymbolicLink()) {
|
||||
writeFileSync(destination, readlinkSync(source));
|
||||
} else if (stats.isFile()) {
|
||||
copyFileSync(source, destination);
|
||||
}
|
||||
}
|
||||
return stagingDirectory;
|
||||
}
|
||||
|
||||
const sourceDirectory = stageScannableSource();
|
||||
const fixtureDirectory = mkdtempSync(join(tmpdir(), 'veritas-gitleaks-regression-'));
|
||||
try {
|
||||
const repositoryScan = runGitleaks(
|
||||
['dir', '.', '--no-banner', '--redact=100', '--gitleaks-ignore-path', '.gitleaksignore'],
|
||||
sourceDirectory
|
||||
);
|
||||
if (repositoryScan.status !== 0) {
|
||||
fail('Gitleaks found a secret outside the reviewed fingerprint baseline.', repositoryScan);
|
||||
}
|
||||
|
||||
const syntheticToken = ['sk', '_live_', '51Z9Y8X7W6V5U4T3S2Q1P0N9'].join('');
|
||||
writeFileSync(join(fixtureDirectory, 'new-secret.env'), `payment_token=${syntheticToken}\n`);
|
||||
const regressionScan = runGitleaks(
|
||||
['dir', '.', '--no-banner', '--redact=100', '--gitleaks-ignore-path', '/dev/null'],
|
||||
fixtureDirectory
|
||||
);
|
||||
if (regressionScan.status !== 1) {
|
||||
fail(
|
||||
`Gitleaks regression fixture was not rejected (expected exit 1, got ${String(regressionScan.status)}).`,
|
||||
regressionScan
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
rmSync(sourceDirectory, { recursive: true, force: true });
|
||||
rmSync(fixtureDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log('Gitleaks accepted the reviewed baseline and rejected a newly introduced test secret.');
|
||||
135
scripts/check-security-gates.mjs
Normal file
135
scripts/check-security-gates.mjs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
function yamlBlock(content, key, indentation) {
|
||||
const prefix = ' '.repeat(indentation);
|
||||
const startPattern = new RegExp(`^${prefix}${key}:\\s*$`, 'm');
|
||||
const match = startPattern.exec(content);
|
||||
if (!match) return '';
|
||||
|
||||
const rest = content.slice(match.index + match[0].length + 1);
|
||||
const endPattern = new RegExp(`^${prefix}\\S`, 'm');
|
||||
const end = endPattern.exec(rest);
|
||||
return end ? rest.slice(0, end.index) : rest;
|
||||
}
|
||||
|
||||
function normalizedPermissionLines(block) {
|
||||
return block
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.sort();
|
||||
}
|
||||
|
||||
export function findSecurityWorkflowViolations(content) {
|
||||
const violations = [];
|
||||
const hasRequiredEvents =
|
||||
/^on:\s*$/m.test(content) &&
|
||||
/^\s{2}pull_request:\s*$/m.test(content) &&
|
||||
/^\s{2}push:\s*$/m.test(content) &&
|
||||
/^\s{4}branches:\s*\[main\]\s*$/m.test(content) &&
|
||||
/^\s{2}schedule:\s*$/m.test(content) &&
|
||||
/^\s{4}- cron:\s*['"][^'"]+['"]\s*$/m.test(content);
|
||||
if (!hasRequiredEvents) {
|
||||
violations.push('security workflow must run for pull requests, main pushes, and a schedule');
|
||||
}
|
||||
|
||||
const topPermissions = normalizedPermissionLines(yamlBlock(content, 'permissions', 0));
|
||||
if (
|
||||
topPermissions.length !== 1 ||
|
||||
topPermissions[0] !== 'contents: read' ||
|
||||
/^permissions:\s+write-all\s*$/m.test(content)
|
||||
) {
|
||||
violations.push('top-level workflow permissions must be contents: read only');
|
||||
}
|
||||
|
||||
const codeqlJob = yamlBlock(content, 'codeql', 2);
|
||||
const codeqlPermissions = normalizedPermissionLines(yamlBlock(codeqlJob, 'permissions', 4));
|
||||
if (
|
||||
codeqlPermissions.length !== 2 ||
|
||||
!codeqlPermissions.includes('contents: read') ||
|
||||
!codeqlPermissions.includes('security-events: write')
|
||||
) {
|
||||
violations.push('CodeQL job must grant only contents: read and security-events: write');
|
||||
}
|
||||
if (
|
||||
!/uses:\s*github\/codeql-action\/init@[0-9a-f]{40}\s+#\s*\S+/.test(codeqlJob) ||
|
||||
!/uses:\s*github\/codeql-action\/analyze@[0-9a-f]{40}\s+#\s*\S+/.test(codeqlJob)
|
||||
) {
|
||||
violations.push('CodeQL init and analyze actions must both be present');
|
||||
}
|
||||
|
||||
const gitleaksJob = yamlBlock(content, 'gitleaks', 2);
|
||||
const gitleaksJobEnvironment = yamlBlock(gitleaksJob, 'env', 4);
|
||||
if (/\$\{\{\s*runner\./.test(gitleaksJobEnvironment)) {
|
||||
violations.push('runner context must not be used in job-level environment values');
|
||||
}
|
||||
const gitleaksPermissions = normalizedPermissionLines(yamlBlock(gitleaksJob, 'permissions', 4));
|
||||
if (
|
||||
gitleaksPermissions.length !== 1 ||
|
||||
gitleaksPermissions[0] !== 'contents: read' ||
|
||||
!/run:\s*pnpm check:gitleaks\s*$/.test(gitleaksJob)
|
||||
) {
|
||||
violations.push('gitleaks job must run pnpm check:gitleaks with contents: read permission');
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function findGitleaksIgnoreViolations(content) {
|
||||
const violations = [];
|
||||
let hasReviewComment = false;
|
||||
|
||||
content.split('\n').forEach((line, index) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
hasReviewComment = false;
|
||||
return;
|
||||
}
|
||||
if (trimmed.startsWith('#')) {
|
||||
hasReviewComment = true;
|
||||
return;
|
||||
}
|
||||
if (!hasReviewComment) {
|
||||
violations.push(
|
||||
`line ${index + 1}: every ignored fingerprint needs a preceding review comment`
|
||||
);
|
||||
}
|
||||
if (!/^[A-Za-z0-9_.\u002f-]+:[a-z0-9-]+:[1-9][0-9]*$/.test(trimmed)) {
|
||||
violations.push(`line ${index + 1}: ignore must be an exact path:rule:line fingerprint`);
|
||||
}
|
||||
});
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function checkSecurityGates({ workflow, gitleaksIgnore }) {
|
||||
return [
|
||||
...findSecurityWorkflowViolations(workflow).map(
|
||||
(message) => `.github/workflows/security.yml: ${message}`
|
||||
),
|
||||
...findGitleaksIgnoreViolations(gitleaksIgnore).map((message) => `.gitleaksignore: ${message}`),
|
||||
];
|
||||
}
|
||||
|
||||
function main() {
|
||||
const violations = checkSecurityGates({
|
||||
workflow: readFileSync('.github/workflows/security.yml', 'utf8'),
|
||||
gitleaksIgnore: readFileSync('.gitleaksignore', 'utf8'),
|
||||
});
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error('Security gate policy violations:');
|
||||
for (const violation of violations) console.error(`- ${violation}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Security workflow and exact gitleaks fingerprint policy verified.');
|
||||
}
|
||||
|
||||
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
||||
main();
|
||||
}
|
||||
83
scripts/check-security-gates.test.mjs
Normal file
83
scripts/check-security-gates.test.mjs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
findGitleaksIgnoreViolations,
|
||||
findSecurityWorkflowViolations,
|
||||
} from './check-security-gates.mjs';
|
||||
|
||||
const SHA = 'db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28';
|
||||
|
||||
test('accepts a least-privilege scheduled CodeQL and gitleaks workflow', () => {
|
||||
const workflow = `
|
||||
name: Security Gates
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '17 9 * * 3'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
codeql:
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: github/codeql-action/init@${SHA} # v4.37.8
|
||||
- uses: github/codeql-action/analyze@${SHA} # v4.37.8
|
||||
gitleaks:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- run: pnpm check:gitleaks
|
||||
`;
|
||||
|
||||
assert.deepEqual(findSecurityWorkflowViolations(workflow), []);
|
||||
});
|
||||
|
||||
test('rejects missing schedules, broad permissions, and incomplete gates', () => {
|
||||
assert.deepEqual(
|
||||
findSecurityWorkflowViolations(`
|
||||
name: Security Gates
|
||||
on: [pull_request]
|
||||
permissions: write-all
|
||||
jobs:
|
||||
codeql:
|
||||
steps:
|
||||
- uses: github/codeql-action/init@v4
|
||||
gitleaks:
|
||||
env:
|
||||
GITLEAKS_BIN: \${{ runner.temp }}/gitleaks
|
||||
`),
|
||||
[
|
||||
'security workflow must run for pull requests, main pushes, and a schedule',
|
||||
'top-level workflow permissions must be contents: read only',
|
||||
'CodeQL job must grant only contents: read and security-events: write',
|
||||
'CodeQL init and analyze actions must both be present',
|
||||
'runner context must not be used in job-level environment values',
|
||||
'gitleaks job must run pnpm check:gitleaks with contents: read permission',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts exact gitleaks fingerprints with review comments', () => {
|
||||
assert.deepEqual(
|
||||
findGitleaksIgnoreViolations(
|
||||
`# Reviewed synthetic fixture\nserver/src/example.test.ts:generic-api-key:42\n`
|
||||
),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects broad or undocumented gitleaks suppressions', () => {
|
||||
assert.deepEqual(
|
||||
findGitleaksIgnoreViolations(`server/.*\n# documented\n.*:generic-api-key:42\n`),
|
||||
[
|
||||
'line 1: every ignored fingerprint needs a preceding review comment',
|
||||
'line 1: ignore must be an exact path:rule:line fingerprint',
|
||||
'line 3: ignore must be an exact path:rule:line fingerprint',
|
||||
]
|
||||
);
|
||||
});
|
||||
156
scripts/check-service-filesystem-boundary.mjs
Normal file
156
scripts/check-service-filesystem-boundary.mjs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#!/usr/bin/env node
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript';
|
||||
|
||||
const ALLOWED_CATEGORIES = new Set([
|
||||
'authoritative-persistence',
|
||||
'packaged-readonly-content',
|
||||
'transient-process-io',
|
||||
'compatibility-debt',
|
||||
]);
|
||||
const FILESYSTEM_MODULE = /^(?:node:)?fs(?:\/promises)?$/;
|
||||
|
||||
export function findDirectFilesystemImports(source) {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
'service.ts',
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TS
|
||||
);
|
||||
const imports = [];
|
||||
const addImport = (moduleSpecifier) => {
|
||||
if (moduleSpecifier && ts.isStringLiteralLike(moduleSpecifier)) {
|
||||
const module = moduleSpecifier.text;
|
||||
if (FILESYSTEM_MODULE.test(module)) {
|
||||
const line = sourceFile.getLineAndCharacterOfPosition(moduleSpecifier.getStart()).line + 1;
|
||||
imports.push({ module, line });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const visit = (node) => {
|
||||
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
||||
addImport(node.moduleSpecifier);
|
||||
} else if (ts.isImportEqualsDeclaration(node)) {
|
||||
if (ts.isExternalModuleReference(node.moduleReference)) {
|
||||
addImport(node.moduleReference.expression);
|
||||
}
|
||||
} else if (ts.isCallExpression(node)) {
|
||||
const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
||||
const isRequire = ts.isIdentifier(node.expression) && node.expression.text === 'require';
|
||||
if (isDynamicImport || isRequire) addImport(node.arguments[0]);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
function collectTypeScriptFiles(directory) {
|
||||
if (!existsSync(directory)) return [];
|
||||
const files = [];
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
if (entry.isDirectory()) files.push(...collectTypeScriptFiles(absolutePath));
|
||||
else if (entry.name.endsWith('.ts')) files.push(absolutePath);
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
export function validateServiceFilesystemBoundary({ root, inventory }) {
|
||||
const violations = [];
|
||||
if (inventory.schemaVersion !== 1) violations.push('inventory schemaVersion must equal 1');
|
||||
if (!Array.isArray(inventory.entries)) violations.push('inventory entries must be an array');
|
||||
if (violations.length > 0) return violations;
|
||||
|
||||
if (inventory.maximumEntries !== inventory.entries.length) {
|
||||
violations.push('maximumEntries must equal the classified entry count');
|
||||
}
|
||||
|
||||
const classified = new Map();
|
||||
for (const entry of inventory.entries) {
|
||||
if (!entry || typeof entry.path !== 'string') {
|
||||
violations.push('every inventory entry must have a path');
|
||||
continue;
|
||||
}
|
||||
if (classified.has(entry.path)) violations.push(`${entry.path}: duplicate inventory entry`);
|
||||
classified.set(entry.path, entry);
|
||||
if (!ALLOWED_CATEGORIES.has(entry.category)) {
|
||||
violations.push(`${entry.path}: invalid category ${JSON.stringify(entry.category)}`);
|
||||
}
|
||||
if (!/^#\d+$/.test(entry.owner ?? '')) {
|
||||
violations.push(`${entry.path}: owner must be a GitHub issue reference`);
|
||||
}
|
||||
if (typeof entry.rationale !== 'string' || entry.rationale.trim().length < 20) {
|
||||
violations.push(`${entry.path}: rationale must explain the temporary exception`);
|
||||
}
|
||||
}
|
||||
|
||||
const detected = new Set();
|
||||
const serviceDirectory = path.join(root, 'server/src/services');
|
||||
for (const absolutePath of collectTypeScriptFiles(serviceDirectory)) {
|
||||
const relativePath = path.relative(root, absolutePath).replaceAll('\\', '/');
|
||||
const imports = findDirectFilesystemImports(readFileSync(absolutePath, 'utf8'));
|
||||
if (imports.length === 0) continue;
|
||||
detected.add(relativePath);
|
||||
if (!classified.has(relativePath)) {
|
||||
violations.push(
|
||||
`${relativePath}:${imports[0].line}: unclassified direct filesystem import (${imports.map((item) => item.module).join(', ')})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const classifiedPath of classified.keys()) {
|
||||
if (!detected.has(classifiedPath)) {
|
||||
violations.push(`${classifiedPath}: stale inventory entry has no direct filesystem import`);
|
||||
}
|
||||
}
|
||||
return violations.sort();
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = {
|
||||
root: process.cwd(),
|
||||
inventory: 'docs/architecture/service-filesystem-boundary.json',
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
if (argv[index] === '--root') options.root = path.resolve(argv[++index]);
|
||||
else if (argv[index] === '--inventory') options.inventory = argv[++index];
|
||||
else throw new Error(`Unknown argument: ${argv[index]}`);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export function runServiceFilesystemBoundaryCheck(argv = process.argv.slice(2)) {
|
||||
let options;
|
||||
try {
|
||||
options = parseArguments(argv);
|
||||
const inventoryPath = path.resolve(options.root, options.inventory);
|
||||
const inventory = JSON.parse(readFileSync(inventoryPath, 'utf8'));
|
||||
const violations = validateServiceFilesystemBoundary({ root: options.root, inventory });
|
||||
if (violations.length > 0) {
|
||||
console.error('Service filesystem boundary check failed.');
|
||||
for (const violation of violations) console.error(`- ${violation}`);
|
||||
process.exitCode = 1;
|
||||
return false;
|
||||
}
|
||||
console.log(
|
||||
`Service filesystem boundary check passed (${inventory.entries.length} classified exceptions).`
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Service filesystem boundary check failed: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isDirectExecution() {
|
||||
return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
}
|
||||
|
||||
if (isDirectExecution()) runServiceFilesystemBoundaryCheck();
|
||||
104
scripts/check-service-filesystem-boundary.test.mjs
Normal file
104
scripts/check-service-filesystem-boundary.test.mjs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import test from 'node:test';
|
||||
|
||||
import { findDirectFilesystemImports } from './check-service-filesystem-boundary.mjs';
|
||||
|
||||
const CHECK_SCRIPT = path.resolve('scripts/check-service-filesystem-boundary.mjs');
|
||||
|
||||
function createFixture({ source, entries = [], servicePath = 'example-service.ts' }) {
|
||||
const root = mkdtempSync(path.join(tmpdir(), 'veritas-service-fs-boundary-'));
|
||||
const serviceDirectory = path.join(root, 'server/src/services');
|
||||
const sourcePath = path.join(serviceDirectory, servicePath);
|
||||
mkdirSync(path.dirname(sourcePath), { recursive: true });
|
||||
writeFileSync(sourcePath, source);
|
||||
writeFileSync(
|
||||
path.join(root, 'service-filesystem-boundary.json'),
|
||||
`${JSON.stringify({ schemaVersion: 1, maximumEntries: entries.length, entries }, null, 2)}\n`
|
||||
);
|
||||
return root;
|
||||
}
|
||||
|
||||
function runCheck(root) {
|
||||
return spawnSync(
|
||||
process.execPath,
|
||||
[CHECK_SCRIPT, '--root', root, '--inventory', 'service-filesystem-boundary.json'],
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
}
|
||||
|
||||
test('rejects a new service filesystem import with a file diagnostic', () => {
|
||||
const root = createFixture({
|
||||
source: "import { readFile } from 'node:fs/promises';\n",
|
||||
});
|
||||
|
||||
const result = runCheck(root);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /server\/src\/services\/example-service\.ts/);
|
||||
assert.match(result.stderr, /unclassified direct filesystem import/);
|
||||
});
|
||||
|
||||
test('scans nested service directories', () => {
|
||||
const root = createFixture({
|
||||
source: "const fs = require('fs');\n",
|
||||
servicePath: 'nested/example-service.ts',
|
||||
});
|
||||
|
||||
const result = runCheck(root);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /server\/src\/services\/nested\/example-service\.ts/);
|
||||
});
|
||||
|
||||
test('ignores filesystem calls embedded in strings and comments', () => {
|
||||
const source = [
|
||||
`const command = "require('node:fs').writeFileSync('/tmp/example', 'x')";`,
|
||||
`// import fs from 'node:fs';`,
|
||||
`/* require('fs/promises') */`,
|
||||
].join('\n');
|
||||
|
||||
assert.deepEqual(findDirectFilesystemImports(source), []);
|
||||
});
|
||||
|
||||
test('detects filesystem re-exports', () => {
|
||||
assert.deepEqual(findDirectFilesystemImports("export { readFile } from 'node:fs';\n"), [
|
||||
{ module: 'node:fs', line: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('accepts a classified direct filesystem import', () => {
|
||||
const entry = {
|
||||
path: 'server/src/services/example-service.ts',
|
||||
category: 'compatibility-debt',
|
||||
owner: '#1189',
|
||||
rationale: 'Migration is tracked by the final storage boundary issue.',
|
||||
};
|
||||
const root = createFixture({
|
||||
source: "const fs = await import('fs/promises');\n",
|
||||
entries: [entry],
|
||||
});
|
||||
|
||||
const result = runCheck(root);
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /1 classified exception/);
|
||||
});
|
||||
|
||||
test('rejects stale inventory entries so the exception count ratchets downward', () => {
|
||||
const entry = {
|
||||
path: 'server/src/services/example-service.ts',
|
||||
category: 'compatibility-debt',
|
||||
owner: '#1189',
|
||||
rationale: 'Migration is tracked by the final storage boundary issue.',
|
||||
};
|
||||
const root = createFixture({ source: 'export const value = 1;\n', entries: [entry] });
|
||||
|
||||
const result = runCheck(root);
|
||||
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /stale inventory entry/);
|
||||
});
|
||||
52
scripts/check-tracked-ignore.mjs
Normal file
52
scripts/check-tracked-ignore.mjs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/env node
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
function runGit(args, options = {}) {
|
||||
const result = spawnSync('git', args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
input: options.input,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function findIgnoredTrackedFiles(cwd = process.cwd(), gitRunner = runGit) {
|
||||
const tracked = gitRunner(['ls-files', '-z'], { cwd });
|
||||
if (tracked.status !== 0) {
|
||||
throw new Error(`git ls-files failed: ${tracked.stderr.toString('utf8').trim()}`);
|
||||
}
|
||||
|
||||
const ignored = gitRunner(['check-ignore', '--no-index', '-z', '--stdin'], {
|
||||
cwd,
|
||||
input: tracked.stdout,
|
||||
});
|
||||
if (ignored.status === 1) return [];
|
||||
if (ignored.status !== 0) {
|
||||
throw new Error(`git check-ignore failed: ${ignored.stderr.toString('utf8').trim()}`);
|
||||
}
|
||||
|
||||
return ignored.stdout.toString('utf8').split('\0').filter(Boolean).sort();
|
||||
}
|
||||
|
||||
export function runTrackedIgnoreCheck(cwd = process.cwd()) {
|
||||
const ignored = findIgnoredTrackedFiles(cwd);
|
||||
if (ignored.length === 0) {
|
||||
console.log('Tracked-file ignore check passed.');
|
||||
return true;
|
||||
}
|
||||
|
||||
console.error('Tracked-file ignore check failed. These tracked paths match ignore rules:');
|
||||
for (const file of ignored) console.error(`- ${file}`);
|
||||
process.exitCode = 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDirectExecution() {
|
||||
return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
}
|
||||
|
||||
if (isDirectExecution()) runTrackedIgnoreCheck();
|
||||
71
scripts/check-tracked-ignore.test.mjs
Normal file
71
scripts/check-tracked-ignore.test.mjs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { findIgnoredTrackedFiles } from './check-tracked-ignore.mjs';
|
||||
|
||||
function result(status, stdout = '', stderr = '') {
|
||||
return {
|
||||
status,
|
||||
stdout: Buffer.from(stdout),
|
||||
stderr: Buffer.from(stderr),
|
||||
error: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function gitRunner(responses, calls) {
|
||||
return (args, options) => {
|
||||
calls.push({ args, options });
|
||||
const response = responses.shift();
|
||||
assert.ok(response, 'unexpected Git invocation');
|
||||
return response;
|
||||
};
|
||||
}
|
||||
|
||||
test('reports sorted tracked paths returned by check-ignore', () => {
|
||||
const calls = [];
|
||||
const tracked = 'server/src/storage/repository.ts\0server/src/__tests__/storage/example.test.ts\0';
|
||||
const ignored =
|
||||
'server/src/storage/repository.ts\0server/src/__tests__/storage/example.test.ts\0';
|
||||
|
||||
assert.deepEqual(
|
||||
findIgnoredTrackedFiles(
|
||||
'/repo',
|
||||
gitRunner([result(0, tracked), result(0, ignored)], calls)
|
||||
),
|
||||
['server/src/__tests__/storage/example.test.ts', 'server/src/storage/repository.ts']
|
||||
);
|
||||
assert.deepEqual(calls[0], {
|
||||
args: ['ls-files', '-z'],
|
||||
options: { cwd: '/repo' },
|
||||
});
|
||||
assert.deepEqual(calls[1], {
|
||||
args: ['check-ignore', '--no-index', '-z', '--stdin'],
|
||||
options: { cwd: '/repo', input: Buffer.from(tracked) },
|
||||
});
|
||||
});
|
||||
|
||||
test('accepts the normal check-ignore no-match exit status', () => {
|
||||
const calls = [];
|
||||
assert.deepEqual(
|
||||
findIgnoredTrackedFiles(
|
||||
'/repo',
|
||||
gitRunner([result(0, 'server/src/storage/repository.ts\0'), result(1)], calls)
|
||||
),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('surfaces Git command failures', () => {
|
||||
assert.throws(
|
||||
() => findIgnoredTrackedFiles('/repo', gitRunner([result(2, '', 'bad index')], [])),
|
||||
/git ls-files failed: bad index/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
findIgnoredTrackedFiles(
|
||||
'/repo',
|
||||
gitRunner([result(0, 'tracked\0'), result(128, '', 'bad ignore rules')], [])
|
||||
),
|
||||
/git check-ignore failed: bad ignore rules/
|
||||
);
|
||||
});
|
||||
45
scripts/coverage-policy-utils.mjs
Normal file
45
scripts/coverage-policy-utils.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import path from 'node:path';
|
||||
|
||||
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const GITHUB_LOGIN_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
||||
const TRACKING_ISSUE_PATTERN = /^#\d+$/;
|
||||
const GLOB_PATTERN = /[*?[\]{}!]/;
|
||||
|
||||
function isValidIsoDate(value) {
|
||||
if (typeof value !== 'string' || !ISO_DATE_PATTERN.test(value)) return false;
|
||||
const date = new Date(`${value}T00:00:00.000Z`);
|
||||
return !Number.isNaN(date.valueOf()) && date.toISOString().slice(0, 10) === value;
|
||||
}
|
||||
|
||||
export function coverageExceptionErrors(exception, label, today) {
|
||||
const errors = [];
|
||||
const normalized = path.posix.normalize(exception.path ?? '');
|
||||
if (
|
||||
!exception.path ||
|
||||
normalized !== exception.path ||
|
||||
normalized.startsWith('../') ||
|
||||
path.posix.isAbsolute(normalized) ||
|
||||
GLOB_PATTERN.test(normalized)
|
||||
) {
|
||||
errors.push(`${label} exception path must be one exact repository-relative file`);
|
||||
}
|
||||
if (typeof exception.reason !== 'string' || exception.reason.trim().length < 20) {
|
||||
errors.push(`${label} exception reason must contain at least 20 characters`);
|
||||
}
|
||||
if (!GITHUB_LOGIN_PATTERN.test(exception.owner ?? '')) {
|
||||
errors.push(`${label} exception owner must be a GitHub login`);
|
||||
}
|
||||
if (!TRACKING_ISSUE_PATTERN.test(exception.trackingIssue ?? '')) {
|
||||
errors.push(`${label} exception must reference a tracking issue such as #123`);
|
||||
}
|
||||
if (!isValidIsoDate(exception.reviewBy)) {
|
||||
errors.push(`${label} exception reviewBy must be a real YYYY-MM-DD date`);
|
||||
return errors;
|
||||
}
|
||||
const maximum = new Date(`${today}T00:00:00.000Z`);
|
||||
maximum.setUTCDate(maximum.getUTCDate() + 90);
|
||||
if (exception.reviewBy < today || exception.reviewBy > maximum.toISOString().slice(0, 10)) {
|
||||
errors.push(`${label} exception reviewBy must be today or within the next 90 days`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
|
@ -11,21 +11,6 @@ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|||
const desktopDir = path.join(rootDir, 'desktop');
|
||||
const releaseDir = path.join(desktopDir, 'release');
|
||||
const requireFromScript = createRequire(import.meta.url);
|
||||
const sensitiveArgumentFlags = new Set([
|
||||
'--apple-id',
|
||||
'--issuer',
|
||||
'--key',
|
||||
'--key-id',
|
||||
'--password',
|
||||
'--team-id',
|
||||
]);
|
||||
|
||||
function sanitizeArgsForError(args) {
|
||||
return args.map((arg, index) =>
|
||||
index > 0 && sensitiveArgumentFlags.has(args[index - 1]) ? '<redacted>' : arg
|
||||
);
|
||||
}
|
||||
|
||||
function run(command, args) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: rootDir,
|
||||
|
|
@ -34,13 +19,11 @@ function run(command, args) {
|
|||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
throw new Error(`${command} failed to start`);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`${command} ${sanitizeArgsForError(args).join(' ')} failed with exit code ${result.status}`
|
||||
);
|
||||
throw new Error(`${command} failed with exit code ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,6 +186,6 @@ async function main() {
|
|||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
console.error(error instanceof Error ? error.message : 'Unknown release finalization error');
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
115
scripts/run-coverage.mjs
Normal file
115
scripts/run-coverage.mjs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env node
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const POLICY_PATH = 'docs/testing/critical-path-coverage.json';
|
||||
const SMOKE_ENV_KEYS = [
|
||||
'HERMES_SMOKE_TEST',
|
||||
'VERITAS_CLAUDE_CODE_SMOKE',
|
||||
'VERITAS_CODEX_APP_SERVER_SMOKE',
|
||||
'VERITAS_RUN_NATIVE_SANDBOX_SMOKE',
|
||||
'VK_MCP_INTEGRATION_TEST',
|
||||
];
|
||||
|
||||
export function parsePackageSelection(args, available) {
|
||||
const valueIndex = args.findIndex((arg) => arg === '--packages');
|
||||
const inline = args.find((arg) => arg.startsWith('--packages='));
|
||||
const raw =
|
||||
inline?.slice('--packages='.length) ?? (valueIndex >= 0 ? args[valueIndex + 1] : undefined);
|
||||
const selected = raw
|
||||
? [
|
||||
...new Set(
|
||||
raw
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
),
|
||||
]
|
||||
: [...available];
|
||||
const known = new Set(available);
|
||||
const unknown = selected.filter((id) => !known.has(id));
|
||||
|
||||
if (unknown.length > 0) {
|
||||
throw new Error(`Unknown coverage package(s): ${unknown.join(', ')}`);
|
||||
}
|
||||
if (selected.length === 0) {
|
||||
throw new Error('At least one coverage package is required.');
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function coverageCommands(policy, selected) {
|
||||
const commands = [['--filter', '@veritas-kanban/shared', 'build']];
|
||||
|
||||
for (const definition of policy.packages) {
|
||||
if (!selected.includes(definition.id)) continue;
|
||||
const runner = definition.runner ?? {};
|
||||
commands.push([
|
||||
'--filter',
|
||||
`@veritas-kanban/${definition.id}`,
|
||||
'exec',
|
||||
'vitest',
|
||||
'run',
|
||||
'--coverage',
|
||||
`--maxWorkers=${runner.maxWorkers ?? 4}`,
|
||||
'--reporter=dot',
|
||||
'--coverage.reporter=json',
|
||||
'--coverage.reporter=json-summary',
|
||||
'--coverage.reporter=html',
|
||||
'--coverage.reporter=text-summary',
|
||||
`--coverage.reportsDirectory=../coverage/${definition.id}`,
|
||||
...(runner.extraArgs ?? []),
|
||||
...(runner.testFiles ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
export function coverageEnvironment(environment = process.env) {
|
||||
const sanitized = { ...environment };
|
||||
for (const key of SMOKE_ENV_KEYS) delete sanitized[key];
|
||||
return {
|
||||
...sanitized,
|
||||
LOG_LEVEL: 'silent',
|
||||
NODE_ENV: 'test',
|
||||
VERITAS_DISABLE_WATCHERS: '1',
|
||||
};
|
||||
}
|
||||
|
||||
function run(command, args, env = process.env) {
|
||||
const executable = process.platform === 'win32' && command === 'pnpm' ? 'pnpm.cmd' : command;
|
||||
const result = spawnSync(executable, args, {
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const repoRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
||||
const policy = JSON.parse(await readFile(path.join(repoRoot, POLICY_PATH), 'utf8'));
|
||||
const selected = parsePackageSelection(
|
||||
process.argv.slice(2),
|
||||
policy.packages.map(({ id }) => id)
|
||||
);
|
||||
const env = coverageEnvironment();
|
||||
|
||||
for (const args of coverageCommands(policy, selected)) run('pnpm', args, env);
|
||||
run('node', ['scripts/check-coverage-ratchets.mjs', '--packages', selected.join(',')], env);
|
||||
}
|
||||
|
||||
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
||||
if (isMain) {
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
81
scripts/run-coverage.test.mjs
Normal file
81
scripts/run-coverage.test.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { coverageCommands, coverageEnvironment, parsePackageSelection } from './run-coverage.mjs';
|
||||
|
||||
const policy = {
|
||||
packages: [
|
||||
{
|
||||
id: 'server',
|
||||
runner: {
|
||||
maxWorkers: 2,
|
||||
extraArgs: ['--testTimeout=30000'],
|
||||
testFiles: ['src/__tests__/provider-runtime.test.ts'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'web',
|
||||
runner: {
|
||||
extraArgs: ['--testTimeout=15000'],
|
||||
testFiles: ['src/__tests__/useWebSocket.test.ts'],
|
||||
},
|
||||
},
|
||||
{ id: 'desktop' },
|
||||
],
|
||||
};
|
||||
|
||||
test('defaults to every applicable workspace and rejects unknown packages', () => {
|
||||
const available = ['server', 'web', 'desktop'];
|
||||
assert.deepEqual(parsePackageSelection([], available), available);
|
||||
assert.deepEqual(parsePackageSelection(['--packages=web,server,web'], available), [
|
||||
'web',
|
||||
'server',
|
||||
]);
|
||||
assert.throws(() => parsePackageSelection(['--packages', 'unknown'], available), /Unknown/);
|
||||
});
|
||||
|
||||
test('builds shared first and emits isolated machine-readable and HTML reports', () => {
|
||||
const commands = coverageCommands(policy, ['web', 'desktop']);
|
||||
assert.deepEqual(commands[0], ['--filter', '@veritas-kanban/shared', 'build']);
|
||||
assert.equal(commands.length, 3);
|
||||
|
||||
const web = commands[1].join(' ');
|
||||
assert.match(web, /--filter @veritas-kanban\/web/);
|
||||
assert.match(web, /--coverage\.reporter=json(?: |$)/);
|
||||
assert.match(web, /--coverage\.reporter=json-summary/);
|
||||
assert.match(web, /--coverage\.reporter=html/);
|
||||
assert.match(web, /--coverage\.reportsDirectory=\.\.\/coverage\/web/);
|
||||
assert.match(web, /--testTimeout=15000/);
|
||||
assert.match(web, /src\/__tests__\/useWebSocket\.test\.ts/);
|
||||
|
||||
const server = coverageCommands(policy, ['server'])[1].join(' ');
|
||||
assert.match(server, /--maxWorkers=2/);
|
||||
assert.match(server, /--testTimeout=30000/);
|
||||
assert.match(server, /src\/__tests__\/provider-runtime\.test\.ts/);
|
||||
|
||||
assert.match(commands[2].join(' '), /--coverage\.reportsDirectory=\.\.\/coverage\/desktop/);
|
||||
});
|
||||
|
||||
test('removes every live integration opt-in from the coverage environment', () => {
|
||||
const environment = coverageEnvironment({
|
||||
PATH: '/bin',
|
||||
HERMES_SMOKE_TEST: 'true',
|
||||
VERITAS_CLAUDE_CODE_SMOKE: '1',
|
||||
VERITAS_CODEX_APP_SERVER_SMOKE: '1',
|
||||
VERITAS_RUN_NATIVE_SANDBOX_SMOKE: '1',
|
||||
VK_MCP_INTEGRATION_TEST: '1',
|
||||
});
|
||||
|
||||
assert.equal(environment.PATH, '/bin');
|
||||
assert.equal(environment.NODE_ENV, 'test');
|
||||
assert.equal(environment.VERITAS_DISABLE_WATCHERS, '1');
|
||||
for (const key of [
|
||||
'HERMES_SMOKE_TEST',
|
||||
'VERITAS_CLAUDE_CODE_SMOKE',
|
||||
'VERITAS_CODEX_APP_SERVER_SMOKE',
|
||||
'VERITAS_RUN_NATIVE_SANDBOX_SMOKE',
|
||||
'VK_MCP_INTEGRATION_TEST',
|
||||
]) {
|
||||
assert.equal(environment[key], undefined);
|
||||
}
|
||||
});
|
||||
121
scripts/run-workspace-unit-tests.mjs
Normal file
121
scripts/run-workspace-unit-tests.mjs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
|
||||
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const pnpmExecutable = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
|
||||
|
||||
export const UNIT_TEST_STAGES = Object.freeze([
|
||||
{
|
||||
id: 'shared-build',
|
||||
label: 'shared prerequisite',
|
||||
directory: 'shared',
|
||||
packageName: '@veritas-kanban/shared',
|
||||
requiredScript: 'build',
|
||||
},
|
||||
{
|
||||
id: 'server',
|
||||
label: 'server unit tests',
|
||||
directory: 'server',
|
||||
packageName: '@veritas-kanban/server',
|
||||
requiredScript: 'test',
|
||||
},
|
||||
{
|
||||
id: 'web',
|
||||
label: 'web unit tests',
|
||||
directory: 'web',
|
||||
packageName: '@veritas-kanban/web',
|
||||
requiredScript: 'test',
|
||||
},
|
||||
{
|
||||
id: 'cli',
|
||||
label: 'CLI unit tests',
|
||||
directory: 'cli',
|
||||
packageName: '@veritas-kanban/cli',
|
||||
requiredScript: 'test',
|
||||
},
|
||||
{
|
||||
id: 'mcp',
|
||||
label: 'MCP unit tests',
|
||||
directory: 'mcp',
|
||||
packageName: '@veritas-kanban/mcp',
|
||||
requiredScript: 'test',
|
||||
},
|
||||
]);
|
||||
|
||||
function readWorkspacePackage(stage) {
|
||||
const packagePath = path.join(repositoryRoot, stage.directory, 'package.json');
|
||||
return JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||
}
|
||||
|
||||
export function validateStage(stage, { readPackageJson = readWorkspacePackage } = {}) {
|
||||
let packageJson;
|
||||
try {
|
||||
packageJson = readPackageJson(stage);
|
||||
} catch (error) {
|
||||
return `Cannot read ${stage.directory}/package.json: ${error.message}`;
|
||||
}
|
||||
if (packageJson.name !== stage.packageName) {
|
||||
return `${stage.directory}/package.json must declare name ${stage.packageName}`;
|
||||
}
|
||||
if (
|
||||
typeof packageJson.scripts?.[stage.requiredScript] !== 'string' ||
|
||||
packageJson.scripts[stage.requiredScript].trim() === ''
|
||||
) {
|
||||
return `${stage.packageName} must define a non-empty ${stage.requiredScript} script`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function executeStage(stage) {
|
||||
const result = spawnSync(pnpmExecutable, ['--filter', stage.packageName, stage.requiredScript], {
|
||||
cwd: repositoryRoot,
|
||||
env: process.env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(`Could not start ${stage.label}: ${result.error.message}`);
|
||||
return 1;
|
||||
}
|
||||
return result.status ?? 1;
|
||||
}
|
||||
|
||||
export function runWorkspaceUnitTests({
|
||||
run = executeStage,
|
||||
validate = validateStage,
|
||||
log = console.log,
|
||||
} = {}) {
|
||||
const results = new Map(UNIT_TEST_STAGES.map((stage) => [stage.id, 'NOT RUN']));
|
||||
let exitCode = 0;
|
||||
|
||||
for (const stage of UNIT_TEST_STAGES) {
|
||||
log(`\n==> ${stage.label}`);
|
||||
const validationError = validate(stage);
|
||||
if (validationError) {
|
||||
log(`Configuration error: ${validationError}`);
|
||||
results.set(stage.id, 'FAIL');
|
||||
exitCode = 1;
|
||||
break;
|
||||
}
|
||||
const status = run(stage);
|
||||
results.set(stage.id, status === 0 ? 'PASS' : 'FAIL');
|
||||
if (status !== 0) {
|
||||
exitCode = status > 0 ? status : 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceSummary = UNIT_TEST_STAGES.filter((stage) => stage.id !== 'shared-build')
|
||||
.map((stage) => `${stage.id}: ${results.get(stage.id)}`)
|
||||
.join(' | ');
|
||||
log(`\nWorkspace unit-test summary: ${workspaceSummary}`);
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
process.exitCode = runWorkspaceUnitTests();
|
||||
}
|
||||
70
scripts/run-workspace-unit-tests.test.mjs
Normal file
70
scripts/run-workspace-unit-tests.test.mjs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
UNIT_TEST_STAGES,
|
||||
runWorkspaceUnitTests,
|
||||
validateStage,
|
||||
} from './run-workspace-unit-tests.mjs';
|
||||
|
||||
test('runs the shared prerequisite and every unit-test workspace in order', () => {
|
||||
const calls = [];
|
||||
const lines = [];
|
||||
|
||||
const status = runWorkspaceUnitTests({
|
||||
run: (stage) => {
|
||||
calls.push(stage.id);
|
||||
return 0;
|
||||
},
|
||||
log: (line) => lines.push(line),
|
||||
});
|
||||
|
||||
assert.equal(status, 0);
|
||||
assert.deepEqual(
|
||||
calls,
|
||||
UNIT_TEST_STAGES.map((stage) => stage.id)
|
||||
);
|
||||
assert.match(lines.at(-1), /server: PASS.*web: PASS.*cli: PASS.*mcp: PASS/);
|
||||
});
|
||||
|
||||
test('stops after the first failure and reports unexecuted workspaces', () => {
|
||||
const calls = [];
|
||||
const lines = [];
|
||||
|
||||
const status = runWorkspaceUnitTests({
|
||||
run: (stage) => {
|
||||
calls.push(stage.id);
|
||||
return stage.id === 'web' ? 7 : 0;
|
||||
},
|
||||
log: (line) => lines.push(line),
|
||||
});
|
||||
|
||||
assert.equal(status, 7);
|
||||
assert.deepEqual(calls, ['shared-build', 'server', 'web']);
|
||||
assert.match(lines.at(-1), /server: PASS.*web: FAIL.*cli: NOT RUN.*mcp: NOT RUN/);
|
||||
});
|
||||
|
||||
test('fails before execution when a workspace test script is missing', () => {
|
||||
const calls = [];
|
||||
const lines = [];
|
||||
|
||||
const status = runWorkspaceUnitTests({
|
||||
validate: (stage) =>
|
||||
validateStage(stage, {
|
||||
readPackageJson: () => ({
|
||||
name: stage.packageName,
|
||||
scripts: { [stage.requiredScript]: stage.id === 'cli' ? '' : 'test-command' },
|
||||
}),
|
||||
}),
|
||||
run: (stage) => {
|
||||
calls.push(stage.id);
|
||||
return 0;
|
||||
},
|
||||
log: (line) => lines.push(line),
|
||||
});
|
||||
|
||||
assert.equal(status, 1);
|
||||
assert.deepEqual(calls, ['shared-build', 'server', 'web']);
|
||||
assert.match(lines.at(-2), /must define a non-empty test script/);
|
||||
assert.match(lines.at(-1), /server: PASS.*web: PASS.*cli: FAIL.*mcp: NOT RUN/);
|
||||
});
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
#!/usr/bin/env node
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { appendFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const WORKSPACE_NAMES = ['server', 'web', 'cli', 'mcp', 'desktop'];
|
||||
|
||||
const FULL_SUITE_PATH_PATTERNS = [
|
||||
/^\.github\/workflows\//,
|
||||
/^scripts\/select-ci-test-scope(?:\.test)?\.mjs$/,
|
||||
/^scripts\/verify-full-suite-job-evidence(?:\.test)?\.mjs$/,
|
||||
];
|
||||
const COVERAGE_POLICY = JSON.parse(
|
||||
readFileSync(
|
||||
fileURLToPath(new URL('../docs/testing/critical-path-coverage.json', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
);
|
||||
const WORKSPACE_NAMES = COVERAGE_POLICY.packages.map(({ id }) => id);
|
||||
|
||||
const ALL_WORKSPACE_PATH_PATTERNS = [
|
||||
/^shared\//,
|
||||
|
|
@ -41,12 +42,6 @@ function normalizeFiles(files) {
|
|||
return [...new Set(files.map((file) => file.trim()).filter(Boolean))].sort();
|
||||
}
|
||||
|
||||
function summarizePaths(files) {
|
||||
const visible = files.slice(0, 5);
|
||||
const remainder = files.length - visible.length;
|
||||
return `${visible.join(', ')}${remainder > 0 ? `, and ${remainder} more` : ''}`;
|
||||
}
|
||||
|
||||
export function isDocumentationPath(file) {
|
||||
return DOCUMENTATION_PATH_PATTERNS.some((pattern) => pattern.test(file));
|
||||
}
|
||||
|
|
@ -55,10 +50,6 @@ export function isDependencyFreeScopeControlPath(file) {
|
|||
return DEPENDENCY_FREE_SCOPE_CONTROL_PATH_PATTERNS.some((pattern) => pattern.test(file));
|
||||
}
|
||||
|
||||
export function requiresFullSuite(file) {
|
||||
return FULL_SUITE_PATH_PATTERNS.some((pattern) => pattern.test(file));
|
||||
}
|
||||
|
||||
export function affectedWorkspaces(files) {
|
||||
const selected = new Set();
|
||||
|
||||
|
|
@ -73,15 +64,17 @@ export function affectedWorkspaces(files) {
|
|||
return WORKSPACE_NAMES.filter((name) => selected.has(name));
|
||||
}
|
||||
|
||||
export function coverageWorkspaces(_files, scope = 'focused') {
|
||||
if (scope === 'full') return [...WORKSPACE_NAMES];
|
||||
return [];
|
||||
}
|
||||
|
||||
export function classifyCiTestScope({
|
||||
eventName,
|
||||
manualScope = '',
|
||||
labels = [],
|
||||
changedFiles = [],
|
||||
deletedFiles = [],
|
||||
reviewedFullSuite = false,
|
||||
reviewedPullRequest = '',
|
||||
reviewedFullSuiteMode = '',
|
||||
}) {
|
||||
const files = normalizeFiles(changedFiles);
|
||||
const deleted = normalizeFiles(deletedFiles);
|
||||
|
|
@ -114,46 +107,20 @@ export function classifyCiTestScope({
|
|||
};
|
||||
}
|
||||
|
||||
if (eventName === 'push' && reviewedFullSuite) {
|
||||
const prSuffix = reviewedPullRequest ? ` for PR #${reviewedPullRequest}` : '';
|
||||
const evidence =
|
||||
reviewedFullSuiteMode === 'identical-tree'
|
||||
? 'has the exact Git tree published by this squash merge'
|
||||
: 'is an ancestor of this merge commit';
|
||||
if (eventName === 'workflow_dispatch' && manualScope === 'focused') {
|
||||
if (packages.length === 0) {
|
||||
return {
|
||||
scope: 'none',
|
||||
packages: [],
|
||||
files,
|
||||
reason: 'The manually selected range does not affect a testable workspace.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
scope: 'none',
|
||||
packages: [],
|
||||
scope: 'focused',
|
||||
packages,
|
||||
files,
|
||||
reason: `The reviewed head${prSuffix} already passed Workspace Unit Tests and ${evidence}.`,
|
||||
};
|
||||
}
|
||||
|
||||
const unclassifiedDeletedCodePaths = deleted.filter(
|
||||
(file) =>
|
||||
!isDocumentationPath(file) &&
|
||||
!isDependencyFreeScopeControlPath(file) &&
|
||||
affectedWorkspaces([file]).length === 0
|
||||
);
|
||||
if (unclassifiedDeletedCodePaths.length > 0) {
|
||||
return {
|
||||
scope: 'full',
|
||||
packages: WORKSPACE_NAMES,
|
||||
files,
|
||||
reason: `Deleted non-documentation paths outside a known workspace fail safe to the full suite: ${summarizePaths(
|
||||
unclassifiedDeletedCodePaths
|
||||
)}`,
|
||||
};
|
||||
}
|
||||
|
||||
const fullSuitePaths = files.filter(requiresFullSuite);
|
||||
if (fullSuitePaths.length > 0) {
|
||||
return {
|
||||
scope: 'full',
|
||||
packages: WORKSPACE_NAMES,
|
||||
files,
|
||||
reason: `CI control paths require the full suite before the selector change can take effect: ${summarizePaths(
|
||||
fullSuitePaths
|
||||
)}`,
|
||||
reason: `Manual dispatch explicitly requested focused diagnostics for: ${packages.join(', ')}.`,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -167,51 +134,24 @@ export function classifyCiTestScope({
|
|||
};
|
||||
}
|
||||
|
||||
if (
|
||||
files.every(
|
||||
(file) => isDocumentationPath(file) || isDependencyFreeScopeControlPath(file)
|
||||
)
|
||||
) {
|
||||
if (files.every((file) => isDocumentationPath(file) || isDependencyFreeScopeControlPath(file))) {
|
||||
return {
|
||||
scope: 'none',
|
||||
packages: [],
|
||||
files,
|
||||
reason:
|
||||
'Only documentation or dependency-free delivery-cadence controls checked by the selector changed.',
|
||||
};
|
||||
}
|
||||
|
||||
const unclassifiedCodePaths = files.filter(
|
||||
(file) =>
|
||||
!isDocumentationPath(file) &&
|
||||
!isDependencyFreeScopeControlPath(file) &&
|
||||
affectedWorkspaces([file]).length === 0
|
||||
);
|
||||
if (unclassifiedCodePaths.length > 0) {
|
||||
return {
|
||||
scope: 'full',
|
||||
packages: WORKSPACE_NAMES,
|
||||
files,
|
||||
reason: `Non-documentation paths outside a known test workspace fail safe to the full suite: ${summarizePaths(
|
||||
unclassifiedCodePaths
|
||||
)}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (packages.length === 0) {
|
||||
return {
|
||||
scope: 'none',
|
||||
packages: [],
|
||||
files,
|
||||
reason: 'No testable workspace changed.',
|
||||
'Only documentation or dependency-free policy controls changed; workspace tests are reserved for explicit milestones.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scope: 'focused',
|
||||
scope: 'none',
|
||||
packages,
|
||||
files,
|
||||
reason: `Run Vitest related coverage for affected workspace packages: ${packages.join(', ')}.`,
|
||||
reason:
|
||||
packages.length > 0
|
||||
? `Ordinary changes defer workspace tests and coverage to an explicit milestone. Affected packages: ${packages.join(', ')}.`
|
||||
: 'This ordinary change is not an explicit test milestone; workspace tests and coverage are deferred.',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -257,11 +197,14 @@ function parseLabels(value) {
|
|||
}
|
||||
}
|
||||
|
||||
function githubOutputLines(result, input) {
|
||||
export function githubOutputLines(result, input) {
|
||||
const diffRange = diffRangeFor(input.baseSha, input.headSha, input.eventName);
|
||||
const coveragePackages = coverageWorkspaces(result.files, result.scope);
|
||||
return [
|
||||
`scope=${result.scope}`,
|
||||
`packages=${result.packages.join(',')}`,
|
||||
`coverage_packages=${coveragePackages.join(',')}`,
|
||||
`base_sha=${input.baseSha}`,
|
||||
`diff_range=${diffRange}`,
|
||||
`reason=${result.reason}`,
|
||||
].join('\n');
|
||||
|
|
@ -304,9 +247,6 @@ async function main() {
|
|||
headSha: process.env.CI_HEAD_SHA || '',
|
||||
manualScope: process.env.CI_MANUAL_SCOPE || '',
|
||||
labels: parseLabels(process.env.CI_PR_LABELS || '[]'),
|
||||
reviewedFullSuite: process.env.CI_REVIEWED_FULL === 'true',
|
||||
reviewedPullRequest: process.env.CI_REVIEWED_PR || '',
|
||||
reviewedFullSuiteMode: process.env.CI_REVIEWED_MODE || '',
|
||||
};
|
||||
|
||||
if (!input.eventName) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import test from 'node:test';
|
|||
import {
|
||||
affectedWorkspaces,
|
||||
classifyCiTestScope,
|
||||
coverageWorkspaces,
|
||||
diffRangeFor,
|
||||
githubOutputLines,
|
||||
isDependencyFreeScopeControlPath,
|
||||
isDocumentationPath,
|
||||
requiresFullSuite,
|
||||
} from './select-ci-test-scope.mjs';
|
||||
|
||||
test('classifies documentation-only pull requests without unit tests', () => {
|
||||
|
|
@ -35,29 +36,52 @@ test('dependency-free cadence controls do not trigger workspace unit tests', ()
|
|||
assert.equal(isDependencyFreeScopeControlPath('scripts/check-delivery-cadence.mjs'), true);
|
||||
});
|
||||
|
||||
test('cadence controls do not widen a focused workspace change', () => {
|
||||
test('cadence controls do not turn an ordinary workspace change into a test milestone', () => {
|
||||
const result = classifyCiTestScope({
|
||||
eventName: 'pull_request',
|
||||
changedFiles: [
|
||||
'scripts/check-delivery-cadence.mjs',
|
||||
'server/src/routes/tasks.ts',
|
||||
],
|
||||
changedFiles: ['scripts/check-delivery-cadence.mjs', 'server/src/routes/tasks.ts'],
|
||||
});
|
||||
|
||||
assert.equal(result.scope, 'focused');
|
||||
assert.equal(result.scope, 'none');
|
||||
assert.deepEqual(result.packages, ['server']);
|
||||
});
|
||||
|
||||
test('selects affected workspaces for ordinary code changes', () => {
|
||||
test('records affected workspaces without testing an ordinary code change', () => {
|
||||
const result = classifyCiTestScope({
|
||||
eventName: 'pull_request',
|
||||
changedFiles: ['web/src/App.tsx', 'server/src/routes/tasks.ts', 'README.md'],
|
||||
});
|
||||
|
||||
assert.equal(result.scope, 'focused');
|
||||
assert.equal(result.scope, 'none');
|
||||
assert.deepEqual(result.packages, ['server', 'web']);
|
||||
});
|
||||
|
||||
test('selects critical coverage only for the full milestone scope', () => {
|
||||
const files = [
|
||||
'server/src/storage/file-storage.ts',
|
||||
'web/src/components/Board.tsx',
|
||||
'mcp/src/tools/tasks.ts',
|
||||
];
|
||||
|
||||
assert.deepEqual(coverageWorkspaces(files), []);
|
||||
assert.deepEqual(coverageWorkspaces(files, 'focused'), []);
|
||||
assert.deepEqual(coverageWorkspaces(files, 'none'), []);
|
||||
assert.deepEqual(coverageWorkspaces(files, 'full'), ['server', 'web', 'cli', 'mcp', 'desktop']);
|
||||
});
|
||||
|
||||
test('defers governed critical-path coverage until a milestone', () => {
|
||||
assert.deepEqual(
|
||||
coverageWorkspaces([
|
||||
'server/src/__tests__/provider-completion-service.test.ts',
|
||||
'server/src/schemas/auth-schemas.ts',
|
||||
'shared/src/utils/api-permissions.ts',
|
||||
'web/src/__tests__/useWebSocket.test.ts',
|
||||
'mcp/src/__tests__/task-tools.test.ts',
|
||||
]),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('ci:full overrides a documentation-only pull request', () => {
|
||||
const result = classifyCiTestScope({
|
||||
eventName: 'pull_request',
|
||||
|
|
@ -68,11 +92,15 @@ test('ci:full overrides a documentation-only pull request', () => {
|
|||
assert.equal(result.scope, 'full');
|
||||
});
|
||||
|
||||
test('selects the full suite only for CI control paths', () => {
|
||||
test('CI control paths require an explicit milestone instead of auto-running the full suite', () => {
|
||||
const paths = [
|
||||
'.github/workflows/ci.yml',
|
||||
'scripts/select-ci-test-scope.mjs',
|
||||
'scripts/verify-full-suite-job-evidence.mjs',
|
||||
'scripts/run-coverage.mjs',
|
||||
'scripts/check-coverage-policy.test.mjs',
|
||||
'docs/testing/critical-path-coverage.json',
|
||||
'web/vitest.config.ts',
|
||||
];
|
||||
|
||||
for (const file of paths) {
|
||||
|
|
@ -80,12 +108,11 @@ test('selects the full suite only for CI control paths', () => {
|
|||
eventName: 'pull_request',
|
||||
changedFiles: [file],
|
||||
});
|
||||
assert.equal(result.scope, 'full', file);
|
||||
assert.equal(requiresFullSuite(file), true, file);
|
||||
assert.equal(result.scope, 'none', file);
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps shared, storage, desktop, and manifest changes focused by workspace', () => {
|
||||
test('records shared, storage, desktop, and manifest workspaces without automatic tests', () => {
|
||||
const cases = [
|
||||
{
|
||||
file: 'shared/src/types/task.types.ts',
|
||||
|
|
@ -103,9 +130,8 @@ test('keeps shared, storage, desktop, and manifest changes focused by workspace'
|
|||
eventName: 'pull_request',
|
||||
changedFiles: [file],
|
||||
});
|
||||
assert.equal(result.scope, 'focused', file);
|
||||
assert.equal(result.scope, 'none', file);
|
||||
assert.deepEqual(result.packages, packages, file);
|
||||
assert.equal(requiresFullSuite(file), false, file);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -140,60 +166,33 @@ test('focused manual runs still classify the selected range by risk', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('a successful reviewed full suite suppresses duplicate post-merge tests', () => {
|
||||
const result = classifyCiTestScope({
|
||||
eventName: 'push',
|
||||
reviewedFullSuite: true,
|
||||
reviewedPullRequest: '1000',
|
||||
changedFiles: ['.github/workflows/ci.yml'],
|
||||
});
|
||||
|
||||
assert.equal(result.scope, 'none');
|
||||
assert.match(result.reason, /PR #1000/);
|
||||
assert.match(result.reason, /ancestor/);
|
||||
});
|
||||
|
||||
test('an exact reviewed tree suppresses duplicate tests after a squash merge', () => {
|
||||
const result = classifyCiTestScope({
|
||||
eventName: 'push',
|
||||
reviewedFullSuite: true,
|
||||
reviewedPullRequest: '1011',
|
||||
reviewedFullSuiteMode: 'identical-tree',
|
||||
changedFiles: ['.github/workflows/ci.yml'],
|
||||
});
|
||||
|
||||
assert.equal(result.scope, 'none');
|
||||
assert.match(result.reason, /PR #1011/);
|
||||
assert.match(result.reason, /exact Git tree/);
|
||||
});
|
||||
|
||||
test('ordinary post-merge pushes remain limited to affected packages', () => {
|
||||
test('ordinary post-merge pushes defer tests while recording affected packages', () => {
|
||||
const result = classifyCiTestScope({
|
||||
eventName: 'push',
|
||||
changedFiles: ['cli/src/commands/doctor.ts'],
|
||||
});
|
||||
|
||||
assert.equal(result.scope, 'focused');
|
||||
assert.equal(result.scope, 'none');
|
||||
assert.deepEqual(result.packages, ['cli']);
|
||||
});
|
||||
|
||||
test('unknown non-documentation paths fail safe to the full suite', () => {
|
||||
test('unknown non-documentation paths wait for an explicit milestone', () => {
|
||||
const result = classifyCiTestScope({
|
||||
eventName: 'pull_request',
|
||||
changedFiles: ['site/src/runtime.ts'],
|
||||
});
|
||||
|
||||
assert.equal(result.scope, 'full');
|
||||
assert.equal(result.scope, 'none');
|
||||
});
|
||||
|
||||
test('deleted known-workspace source stays focused while unknown source fails safe', () => {
|
||||
test('deleted source paths are recorded without automatic test escalation', () => {
|
||||
assert.equal(
|
||||
classifyCiTestScope({
|
||||
eventName: 'pull_request',
|
||||
changedFiles: ['server/src/obsolete.ts'],
|
||||
deletedFiles: ['server/src/obsolete.ts'],
|
||||
}).scope,
|
||||
'focused'
|
||||
'none'
|
||||
);
|
||||
assert.equal(
|
||||
classifyCiTestScope({
|
||||
|
|
@ -201,7 +200,7 @@ test('deleted known-workspace source stays focused while unknown source fails sa
|
|||
changedFiles: ['site/src/obsolete.ts'],
|
||||
deletedFiles: ['site/src/obsolete.ts'],
|
||||
}).scope,
|
||||
'full'
|
||||
'none'
|
||||
);
|
||||
assert.equal(
|
||||
classifyCiTestScope({
|
||||
|
|
@ -247,3 +246,14 @@ test('uses two-dot push ranges, three-dot review ranges, and rejects non-SHAs',
|
|||
/hexadecimal commit IDs/
|
||||
);
|
||||
});
|
||||
|
||||
test('exports the resolved base SHA for downstream coverage gates', () => {
|
||||
const baseSha = 'a'.repeat(40);
|
||||
const headSha = 'b'.repeat(40);
|
||||
const output = githubOutputLines(
|
||||
{ scope: 'focused', packages: ['server'], files: [], reason: 'test' },
|
||||
{ baseSha, headSha, eventName: 'workflow_dispatch' }
|
||||
);
|
||||
|
||||
assert.match(output, new RegExp(`^base_sha=${baseSha}$`, 'm'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,8 +7,13 @@ const REQUIRED_FULL_SUITE_STEPS = [
|
|||
'Run desktop readiness regression tests',
|
||||
'Run dual-storage parity tests',
|
||||
];
|
||||
const REQUIRED_COVERAGE_STEPS = [
|
||||
'Verify coverage policy',
|
||||
'Measure and ratchet critical paths',
|
||||
'Upload coverage reports',
|
||||
];
|
||||
|
||||
export function hasSuccessfulFullSuiteEvidence(job) {
|
||||
function hasSuccessfulJobEvidence(job, requiredSteps) {
|
||||
if (
|
||||
!job ||
|
||||
typeof job !== 'object' ||
|
||||
|
|
@ -19,7 +24,7 @@ export function hasSuccessfulFullSuiteEvidence(job) {
|
|||
return false;
|
||||
}
|
||||
|
||||
return REQUIRED_FULL_SUITE_STEPS.every((requiredName) =>
|
||||
return requiredSteps.every((requiredName) =>
|
||||
job.steps.some(
|
||||
(step) =>
|
||||
step &&
|
||||
|
|
@ -31,6 +36,14 @@ export function hasSuccessfulFullSuiteEvidence(job) {
|
|||
);
|
||||
}
|
||||
|
||||
export function hasSuccessfulFullSuiteEvidence(job) {
|
||||
return hasSuccessfulJobEvidence(job, REQUIRED_FULL_SUITE_STEPS);
|
||||
}
|
||||
|
||||
export function hasSuccessfulCoverageEvidence(job) {
|
||||
return hasSuccessfulJobEvidence(job, REQUIRED_COVERAGE_STEPS);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let job;
|
||||
|
||||
|
|
@ -45,7 +58,9 @@ async function main() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!hasSuccessfulFullSuiteEvidence(job)) {
|
||||
const validator =
|
||||
process.argv[2] === 'coverage' ? hasSuccessfulCoverageEvidence : hasSuccessfulFullSuiteEvidence;
|
||||
if (!validator(job)) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { spawnSync } from 'node:child_process';
|
|||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { hasSuccessfulFullSuiteEvidence } from './verify-full-suite-job-evidence.mjs';
|
||||
import {
|
||||
hasSuccessfulCoverageEvidence,
|
||||
hasSuccessfulFullSuiteEvidence,
|
||||
} from './verify-full-suite-job-evidence.mjs';
|
||||
|
||||
const scriptPath = fileURLToPath(new URL('./verify-full-suite-job-evidence.mjs', import.meta.url));
|
||||
|
||||
|
|
@ -67,3 +70,25 @@ test('command-line entrypoint validates piped GitHub job JSON', () => {
|
|||
});
|
||||
assert.equal(rejected.status, 1, rejected.stderr);
|
||||
});
|
||||
|
||||
test('requires successful policy, ratchet, and artifact steps for coverage evidence', () => {
|
||||
const coverageJob = job({
|
||||
steps: [
|
||||
{ name: 'Verify coverage policy', status: 'completed', conclusion: 'success' },
|
||||
{ name: 'Measure and ratchet critical paths', status: 'completed', conclusion: 'success' },
|
||||
{ name: 'Upload coverage reports', status: 'completed', conclusion: 'success' },
|
||||
],
|
||||
});
|
||||
assert.equal(hasSuccessfulCoverageEvidence(coverageJob), true);
|
||||
coverageJob.steps[2].conclusion = 'skipped';
|
||||
assert.equal(hasSuccessfulCoverageEvidence(coverageJob), false);
|
||||
|
||||
const accepted = spawnSync(process.execPath, [scriptPath, 'coverage'], {
|
||||
input: JSON.stringify({
|
||||
...coverageJob,
|
||||
steps: coverageJob.steps.map((step) => ({ ...step, conclusion: 'success' })),
|
||||
}),
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.equal(accepted.status, 0, accepted.stderr);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@veritas-kanban/server",
|
||||
"version": "6.1.1",
|
||||
"version": "6.1.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
@ -10,32 +10,31 @@
|
|||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"test": "VERITAS_DISABLE_WATCHERS=1 vitest run",
|
||||
"test": "VERITAS_DISABLE_WATCHERS=1 vitest run --maxWorkers=4",
|
||||
"test:watch": "vitest",
|
||||
"clean": "rm -rf dist",
|
||||
"reset-password": "tsx src/scripts/reset-password.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openai/codex-sdk": "0.147.0",
|
||||
"@openai/codex-sdk": "0.148.0",
|
||||
"@veritas-kanban/shared": "workspace:*",
|
||||
"ajv": "^8.20.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"compression": "^1.8.1",
|
||||
"content-disposition": "^2.0.1",
|
||||
"content-disposition": "^3.0.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.4.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.6.2",
|
||||
"file-type": "^22.0.1",
|
||||
"file-type": "^22.0.2",
|
||||
"helmet": "^8.3.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mammoth": "^1.12.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"multer": "^2.2.0",
|
||||
"nanoid": "^6.0.1",
|
||||
"nostr-tools": "2.24.1",
|
||||
"nostr-tools": "2.24.3",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"sanitize-filename": "^1.6.4",
|
||||
|
|
@ -56,14 +55,13 @@
|
|||
"@types/express": "^5.0.0",
|
||||
"@types/express-serve-static-core": "^5.1.3",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/multer": "^2.2.0",
|
||||
"@types/sanitize-html": "^2.16.1",
|
||||
"@types/supertest": "^7.2.1",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"@vitest/coverage-v8": "^4.1.11",
|
||||
"eslint": "^10.8.1",
|
||||
"supertest": "^7.2.2",
|
||||
"tsx": "^4.23.12",
|
||||
|
|
|
|||
175
server/src/__tests__/agent-permission-repository.test.ts
Normal file
175
server/src/__tests__/agent-permission-repository.test.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { lstat, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
AgentPermissionService,
|
||||
type AgentPermissionConfig,
|
||||
type ApprovalRequest,
|
||||
} from '../services/agent-permission-service.js';
|
||||
import {
|
||||
FileAgentPermissionRepository,
|
||||
InMemoryAgentPermissionRepository,
|
||||
} from '../storage/agent-permission-repository.js';
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
||||
return { ...actual, lstat: vi.fn(actual.lstat) };
|
||||
});
|
||||
|
||||
function permission(agentId: string): AgentPermissionConfig {
|
||||
return {
|
||||
agentId,
|
||||
level: 'specialist',
|
||||
canCreateTasks: true,
|
||||
canDelegate: false,
|
||||
canApprove: false,
|
||||
autoComplete: true,
|
||||
updatedAt: '2026-08-23T20:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
function approval(id: string): ApprovalRequest {
|
||||
return {
|
||||
id,
|
||||
agentId: 'tars',
|
||||
action: 'create_task',
|
||||
status: 'pending',
|
||||
createdAt: '2026-08-23T20:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
describe('FileAgentPermissionRepository', () => {
|
||||
let root: string;
|
||||
let runtimeDir: string;
|
||||
let repository: FileAgentPermissionRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(path.join(process.cwd(), '.veritas-agent-permission-'));
|
||||
runtimeDir = path.join(root, 'runtime');
|
||||
repository = new FileAgentPermissionRepository(runtimeDir, []);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('serializes concurrent permission and approval mutations', async () => {
|
||||
await expect(repository.readPermissions()).resolves.toEqual([]);
|
||||
await expect(repository.readApprovals()).resolves.toEqual([]);
|
||||
await Promise.all([
|
||||
repository.mutatePermissions((values) => ({
|
||||
values: [...values, permission('tars')],
|
||||
result: undefined,
|
||||
})),
|
||||
repository.mutatePermissions((values) => ({
|
||||
values: [...values, permission('case')],
|
||||
result: undefined,
|
||||
})),
|
||||
repository.mutateApprovals((values) => ({
|
||||
values: [...values, approval('one')],
|
||||
result: undefined,
|
||||
})),
|
||||
repository.mutateApprovals((values) => ({
|
||||
values: [...values, approval('two')],
|
||||
result: undefined,
|
||||
})),
|
||||
]);
|
||||
expect((await repository.readPermissions()).map(({ agentId }) => agentId)).toEqual(
|
||||
expect.arrayContaining(['tars', 'case'])
|
||||
);
|
||||
expect((await repository.readApprovals()).map(({ id }) => id)).toEqual(
|
||||
expect.arrayContaining(['one', 'two'])
|
||||
);
|
||||
});
|
||||
|
||||
it('migrates legacy state and tolerates malformed or non-array JSON', async () => {
|
||||
const legacyDir = path.join(root, 'legacy');
|
||||
await mkdir(legacyDir);
|
||||
await writeFile(
|
||||
path.join(legacyDir, 'agent-permissions.json'),
|
||||
JSON.stringify([permission('legacy')]),
|
||||
'utf8'
|
||||
);
|
||||
await writeFile(
|
||||
path.join(legacyDir, 'approval-requests.json'),
|
||||
JSON.stringify([approval('legacy')]),
|
||||
'utf8'
|
||||
);
|
||||
const migratingRepository = new FileAgentPermissionRepository(runtimeDir, [legacyDir]);
|
||||
await expect(migratingRepository.readPermissions()).resolves.toEqual([permission('legacy')]);
|
||||
await expect(migratingRepository.readApprovals()).resolves.toEqual([approval('legacy')]);
|
||||
|
||||
await writeFile(path.join(runtimeDir, 'agent-permissions.json'), '{broken', 'utf8');
|
||||
await expect(repository.readPermissions()).resolves.toEqual([]);
|
||||
await writeFile(path.join(runtimeDir, 'agent-permissions.json'), '{}', 'utf8');
|
||||
await expect(repository.readPermissions()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects symbolic links, changed files, and non-file paths', async () => {
|
||||
await mkdir(runtimeDir, { recursive: true });
|
||||
const stateFile = path.join(runtimeDir, 'agent-permissions.json');
|
||||
const target = path.join(root, 'outside.json');
|
||||
await writeFile(target, '[]', 'utf8');
|
||||
await symlink(target, stateFile);
|
||||
await expect(repository.readPermissions()).rejects.toThrow(/symbolic link/i);
|
||||
|
||||
await rm(stateFile);
|
||||
await writeFile(stateFile, '[]', 'utf8');
|
||||
const actual = await vi.importActual<typeof import('node:fs/promises')>('node:fs/promises');
|
||||
vi.mocked(lstat).mockImplementationOnce(async (filePath) => {
|
||||
const stats = await actual.lstat(filePath);
|
||||
return Object.assign(Object.create(Object.getPrototypeOf(stats)), stats, {
|
||||
ino: stats.ino + 1,
|
||||
});
|
||||
});
|
||||
await expect(repository.readPermissions()).rejects.toThrow(/changed file/i);
|
||||
|
||||
await rm(stateFile);
|
||||
await mkdir(stateFile);
|
||||
await expect(repository.readPermissions()).rejects.toThrow(/bounded regular file/i);
|
||||
});
|
||||
|
||||
it('rejects symbolic-link directories and oversized state', async () => {
|
||||
const realDirectory = path.join(root, 'real-runtime');
|
||||
const linkedDirectory = path.join(root, 'linked-runtime');
|
||||
await mkdir(realDirectory);
|
||||
await symlink(realDirectory, linkedDirectory, 'dir');
|
||||
const linkedRepository = new FileAgentPermissionRepository(linkedDirectory, []);
|
||||
await expect(
|
||||
linkedRepository.mutatePermissions(() => ({ values: [permission('unsafe')], result: null }))
|
||||
).rejects.toThrow(/regular directory/i);
|
||||
|
||||
await expect(
|
||||
repository.mutatePermissions(() => ({
|
||||
values: [{ ...permission('large'), restrictions: ['x'.repeat(16 * 1024 * 1024)] }],
|
||||
result: null,
|
||||
}))
|
||||
).rejects.toThrow(/16 MiB/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentPermissionService storage integration', () => {
|
||||
it('manages levels, restrictions, and approval requests through the repository', async () => {
|
||||
const repository = new InMemoryAgentPermissionRepository();
|
||||
const service = new AgentPermissionService(repository);
|
||||
await expect(service.getPermissions('TARS')).resolves.toMatchObject({
|
||||
agentId: 'tars',
|
||||
level: 'specialist',
|
||||
});
|
||||
|
||||
await service.setLevel('TARS', 'intern');
|
||||
await service.updatePermissions('TARS', { restrictions: ['deploy'] });
|
||||
await expect(service.checkPermission('TARS', 'deploy_production')).resolves.toMatchObject({
|
||||
allowed: false,
|
||||
});
|
||||
await expect(service.listPermissions()).resolves.toHaveLength(1);
|
||||
|
||||
const request = await service.requestApproval({ agentId: 'TARS', action: 'create_task' });
|
||||
await expect(service.getPendingApprovals({ agentId: 'TARS' })).resolves.toHaveLength(1);
|
||||
await expect(service.reviewApproval(request.id, 'approved', 'brad')).resolves.toMatchObject({
|
||||
status: 'approved',
|
||||
reviewedBy: 'brad',
|
||||
});
|
||||
await expect(service.getPendingApprovals()).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
177
server/src/__tests__/agent-provider-adapter-registry.test.ts
Normal file
177
server/src/__tests__/agent-provider-adapter-registry.test.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { EventEmitter } from 'node:events';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { EXECUTABLE_AGENT_PROVIDERS, type ExecutableAgentProvider } from '@veritas-kanban/shared';
|
||||
import {
|
||||
AgentProviderAdapterRegistry,
|
||||
type AgentProviderAdapterHost,
|
||||
type AgentProviderStartContext,
|
||||
} from '../services/agent-provider-adapter-registry.js';
|
||||
import { providerRuntimeManifestFixture } from './fixtures/provider-runtime-manifest.js';
|
||||
|
||||
const RENDERER_NAMES: Record<ExecutableAgentProvider, string> = {
|
||||
'codex-cli': 'renderCodexCliTaskEnvelope',
|
||||
'codex-sdk': 'renderCodexSdkTaskEnvelope',
|
||||
'codex-app-server': 'renderCodexAppServerTaskEnvelope',
|
||||
'acp-stdio': 'renderAcpStdioTaskEnvelope',
|
||||
'claude-code': 'renderClaudeCodeTaskEnvelope',
|
||||
'hermes-cli': 'renderHermesTaskEnvelope',
|
||||
openclaw: 'renderOpenClawTaskEnvelope',
|
||||
};
|
||||
|
||||
function createHost(): AgentProviderAdapterHost {
|
||||
return {
|
||||
probe: vi.fn(async (provider) =>
|
||||
providerRuntimeManifestFixture({ provider, adapter: provider })
|
||||
),
|
||||
probeAcp: vi.fn(async () =>
|
||||
providerRuntimeManifestFixture({ provider: 'acp-stdio', adapter: 'acp-stdio' })
|
||||
),
|
||||
assertTransport: vi.fn(),
|
||||
getPending: vi.fn(),
|
||||
startCodexCli: vi.fn(async () => undefined),
|
||||
startCodexSdk: vi.fn(async () => undefined),
|
||||
handleCodexSdkError: vi.fn(async () => undefined),
|
||||
startCodexAppServer: vi.fn(async () => undefined),
|
||||
startAcpStdio: vi.fn(async () => undefined),
|
||||
startClaudeCode: vi.fn(async () => undefined),
|
||||
startHermesCli: vi.fn(async () => undefined),
|
||||
startOpenClaw: vi.fn(async () => undefined),
|
||||
warn: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function startContext(provider: ExecutableAgentProvider): AgentProviderStartContext {
|
||||
return {
|
||||
task: { id: 'task_provider_registry' },
|
||||
transport: {
|
||||
schemaVersion: 'provider-task-envelope-transport/v1',
|
||||
provider,
|
||||
taskEnvelopeDigest: 'task-envelope-digest',
|
||||
callbackPosture: provider === 'openclaw' ? 'veritas-http' : 'harness-owned',
|
||||
completionNormalization: 'harness',
|
||||
content: 'Run the task.',
|
||||
},
|
||||
logPath: '/tmp/provider-registry.log',
|
||||
attemptId: 'attempt_provider_registry',
|
||||
startedAt: '2026-08-24T06:00:00.000Z',
|
||||
emitter: new EventEmitter(),
|
||||
attempt: { id: 'attempt_provider_registry', status: 'running', agent: 'codex' },
|
||||
runLaunchManifest: { digest: 'run-launch-digest' },
|
||||
conversation: {
|
||||
schemaVersion: 'conversation-lifecycle/v1',
|
||||
mode: 'fresh',
|
||||
intent: 'fresh',
|
||||
state: 'active',
|
||||
contextWindow: { posture: 'unknown', measuredAt: '2026-08-24T06:00:00.000Z' },
|
||||
createdAt: '2026-08-24T06:00:00.000Z',
|
||||
updatedAt: '2026-08-24T06:00:00.000Z',
|
||||
},
|
||||
admission: {
|
||||
schemaVersion: 'provider-admission-evidence/v1',
|
||||
source: 'direct',
|
||||
outcome: 'admitted',
|
||||
reservationId: 'reservation_provider_registry',
|
||||
executionTree: {
|
||||
rootObjectiveId: 'objective_provider_registry',
|
||||
nodeId: 'node_provider_registry',
|
||||
depth: 0,
|
||||
edge: 'root',
|
||||
},
|
||||
},
|
||||
} as AgentProviderStartContext;
|
||||
}
|
||||
|
||||
describe('AgentProviderAdapterRegistry', () => {
|
||||
it('resolves every executable provider without an implicit fallback', async () => {
|
||||
const host = createHost();
|
||||
const registry = new AgentProviderAdapterRegistry(host);
|
||||
|
||||
for (const provider of EXECUTABLE_AGENT_PROVIDERS) {
|
||||
const adapter = registry.resolve(provider);
|
||||
|
||||
expect(adapter.id).toBe(provider);
|
||||
expect(adapter.renderTaskEnvelope.name).toBe(RENDERER_NAMES[provider]);
|
||||
expect(adapter.runEventMapper.mapEvent).toEqual(expect.any(Function));
|
||||
await expect(
|
||||
adapter.probe({
|
||||
health: {
|
||||
type: provider,
|
||||
name: provider,
|
||||
enabled: true,
|
||||
configured: true,
|
||||
command: provider,
|
||||
executableFound: true,
|
||||
authenticated: true,
|
||||
healthy: true,
|
||||
checkedAt: '2026-08-24T06:00:00.000Z',
|
||||
},
|
||||
})
|
||||
).resolves.toMatchObject({ provider, adapter: provider });
|
||||
}
|
||||
|
||||
expect(host.probeAcp).toHaveBeenCalledOnce();
|
||||
expect(host.probe).toHaveBeenCalledTimes(EXECUTABLE_AGENT_PROVIDERS.length - 1);
|
||||
});
|
||||
|
||||
it('dispatches starts through the exact provider operation', async () => {
|
||||
const host = createHost();
|
||||
const registry = new AgentProviderAdapterRegistry(host);
|
||||
const startOperations: Record<ExecutableAgentProvider, ReturnType<typeof vi.fn>> = {
|
||||
'codex-cli': host.startCodexCli,
|
||||
'codex-sdk': host.startCodexSdk,
|
||||
'codex-app-server': host.startCodexAppServer,
|
||||
'acp-stdio': host.startAcpStdio,
|
||||
'claude-code': host.startClaudeCode,
|
||||
'hermes-cli': host.startHermesCli,
|
||||
openclaw: host.startOpenClaw,
|
||||
};
|
||||
|
||||
for (const provider of EXECUTABLE_AGENT_PROVIDERS) {
|
||||
await registry.resolve(provider).start(startContext(provider));
|
||||
expect(startOperations[provider]).toHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
expect(host.assertTransport).toHaveBeenCalledTimes(EXECUTABLE_AGENT_PROVIDERS.length);
|
||||
});
|
||||
|
||||
it('keeps stop behavior behind the adapter seam', async () => {
|
||||
const host = createHost();
|
||||
const registry = new AgentProviderAdapterRegistry(host);
|
||||
const abortController = new AbortController();
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const close = vi.fn(async () => undefined);
|
||||
|
||||
await registry.resolve('codex-sdk').stop({
|
||||
taskId: 'task_provider_registry',
|
||||
pending: {
|
||||
taskId: 'task_provider_registry',
|
||||
attemptId: 'attempt_provider_registry',
|
||||
abortController,
|
||||
},
|
||||
});
|
||||
await registry.resolve('acp-stdio').stop({
|
||||
taskId: 'task_provider_registry',
|
||||
pending: {
|
||||
taskId: 'task_provider_registry',
|
||||
attemptId: 'attempt_provider_registry',
|
||||
acpControl: { cancel, close },
|
||||
},
|
||||
});
|
||||
await registry.resolve('openclaw').stop({
|
||||
taskId: 'task_provider_registry',
|
||||
pending: {
|
||||
taskId: 'task_provider_registry',
|
||||
attemptId: 'attempt_provider_registry',
|
||||
openclawSessionKey: 'session_provider_registry',
|
||||
},
|
||||
});
|
||||
|
||||
expect(abortController.signal.aborted).toBe(true);
|
||||
expect(cancel).toHaveBeenCalledOnce();
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
expect(host.warn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ sessionKey: 'session_provider_registry' }),
|
||||
expect.stringContaining('OpenClaw stop requested')
|
||||
);
|
||||
});
|
||||
});
|
||||
473
server/src/__tests__/attempt-lifecycle-coordinator.test.ts
Normal file
473
server/src/__tests__/attempt-lifecycle-coordinator.test.ts
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type {
|
||||
ProviderRuntimeManifest,
|
||||
Task,
|
||||
TaskAttempt,
|
||||
TaskEnvelope,
|
||||
UpdateTaskInput,
|
||||
} from '@veritas-kanban/shared';
|
||||
import { providerRuntimeManifestFixture } from './fixtures/provider-runtime-manifest.js';
|
||||
import {
|
||||
AttemptLifecycleCoordinator,
|
||||
type AttemptLifecycleStore,
|
||||
} from '../services/attempt-lifecycle-coordinator.js';
|
||||
import { ProviderCompletionService } from '../services/provider-completion-service.js';
|
||||
import { TaskEnvelopeService } from '../services/task-envelope-service.js';
|
||||
|
||||
const completedAt = '2026-08-24T05:00:00.000Z';
|
||||
|
||||
function baseTask(): Task {
|
||||
return {
|
||||
id: 'task_lifecycle_completion',
|
||||
title: 'Persist completion through one lifecycle owner',
|
||||
description: 'Keep terminal attempt mutation behind the lifecycle coordinator.',
|
||||
type: 'code',
|
||||
status: 'in-progress',
|
||||
priority: 'high',
|
||||
project: 'veritas-kanban',
|
||||
created: '2026-08-24T04:00:00.000Z',
|
||||
updated: '2026-08-24T04:00:00.000Z',
|
||||
revision: 4,
|
||||
executionPolicy: { commitPolicy: 'allowed' },
|
||||
};
|
||||
}
|
||||
|
||||
async function taskEnvelope(
|
||||
task: Task,
|
||||
providerRuntimeManifest: ProviderRuntimeManifest
|
||||
): Promise<TaskEnvelope> {
|
||||
return new TaskEnvelopeService({
|
||||
captureLaunchBaseline: async (_worktreePath, capturedAt) => ({
|
||||
capturedAt,
|
||||
headSha: 'a'.repeat(40),
|
||||
dirty: false,
|
||||
files: [],
|
||||
}),
|
||||
captureCompletionEvidence: async () => ({
|
||||
capturedAt: completedAt,
|
||||
headSha: 'b'.repeat(40),
|
||||
changedFiles: [],
|
||||
commits: [],
|
||||
artifacts: [],
|
||||
verification: [],
|
||||
sideEffects: [],
|
||||
}),
|
||||
}).build({
|
||||
task,
|
||||
attemptId: 'attempt_lifecycle_completion',
|
||||
createdAt: '2026-08-24T04:30:00.000Z',
|
||||
worktreePath: '/tmp/veritas-attempt-lifecycle',
|
||||
providerRuntimeManifest,
|
||||
commitPolicy: 'allowed',
|
||||
});
|
||||
}
|
||||
|
||||
class MemoryAttemptLifecycleStore implements AttemptLifecycleStore {
|
||||
constructor(private task: Task) {}
|
||||
|
||||
async getTask(taskId: string): Promise<Task | null> {
|
||||
return taskId === this.task.id ? structuredClone(this.task) : null;
|
||||
}
|
||||
|
||||
async updateTask(taskId: string, input: UpdateTaskInput): Promise<Task | null> {
|
||||
if (taskId !== this.task.id) return null;
|
||||
if (input.expectedRevision !== undefined && input.expectedRevision !== this.task.revision) {
|
||||
throw new Error('Task revision conflict');
|
||||
}
|
||||
const { expectedRevision: _expectedRevision, ...patch } = input;
|
||||
this.task = {
|
||||
...this.task,
|
||||
...patch,
|
||||
revision: (this.task.revision ?? 0) + 1,
|
||||
updated: completedAt,
|
||||
};
|
||||
return structuredClone(this.task);
|
||||
}
|
||||
|
||||
async patchTaskAttempt(
|
||||
taskId: string,
|
||||
attemptId: string,
|
||||
patch: Partial<Omit<TaskAttempt, 'id'>>
|
||||
): Promise<Task | null> {
|
||||
if (taskId !== this.task.id || this.task.attempt?.id !== attemptId) return null;
|
||||
this.task = {
|
||||
...this.task,
|
||||
attempt: { ...this.task.attempt, ...patch },
|
||||
revision: (this.task.revision ?? 0) + 1,
|
||||
updated: completedAt,
|
||||
};
|
||||
return structuredClone(this.task);
|
||||
}
|
||||
}
|
||||
|
||||
async function completionFixture(summary = 'Lifecycle work completed.') {
|
||||
const task = baseTask();
|
||||
const providerRuntimeManifest = providerRuntimeManifestFixture();
|
||||
const envelope = await taskEnvelope(task, providerRuntimeManifest);
|
||||
const attempt: TaskAttempt = {
|
||||
id: envelope.attempt.id,
|
||||
agent: 'codex',
|
||||
provider: envelope.launchManifest.provider,
|
||||
status: 'running',
|
||||
started: envelope.createdAt,
|
||||
providerRuntimeManifest,
|
||||
taskEnvelope: envelope,
|
||||
};
|
||||
const activeTask: Task = { ...task, attempt, attempts: [attempt] };
|
||||
const completionResult = await new ProviderCompletionService(
|
||||
{
|
||||
captureCompletionEvidence: async () => ({
|
||||
capturedAt: completedAt,
|
||||
headSha: 'b'.repeat(40),
|
||||
changedFiles: [],
|
||||
commits: [],
|
||||
artifacts: [],
|
||||
verification: [],
|
||||
sideEffects: [],
|
||||
}),
|
||||
},
|
||||
() => completedAt
|
||||
).complete({
|
||||
task: activeTask,
|
||||
taskEnvelope: envelope,
|
||||
claim: {
|
||||
terminalSource: 'process',
|
||||
status: 'success',
|
||||
summary,
|
||||
},
|
||||
});
|
||||
return { task: activeTask, attempt, completionResult };
|
||||
}
|
||||
|
||||
describe('AttemptLifecycleCoordinator', () => {
|
||||
it('persists an active-attempt transition with revision and history invariants', async () => {
|
||||
const task = baseTask();
|
||||
const attempt: TaskAttempt = {
|
||||
id: 'attempt_active',
|
||||
agent: 'codex',
|
||||
provider: 'codex-cli',
|
||||
status: 'running',
|
||||
};
|
||||
const activeTask = { ...task, attempt, attempts: [attempt] };
|
||||
const store = new MemoryAttemptLifecycleStore(activeTask);
|
||||
const coordinator = new AttemptLifecycleCoordinator(store);
|
||||
const recoveredAttempt: TaskAttempt = {
|
||||
...attempt,
|
||||
runRecovery: {
|
||||
code: 'terminal-result-missing',
|
||||
detail: 'Supervisor has no terminal result.',
|
||||
nextAction: 'Inspect the provider log.',
|
||||
recordedAt: completedAt,
|
||||
},
|
||||
};
|
||||
|
||||
const updated = await coordinator.persistActiveAttempt({
|
||||
task: activeTask,
|
||||
attempt: recoveredAttempt,
|
||||
status: 'blocked',
|
||||
});
|
||||
|
||||
expect(updated).toMatchObject({
|
||||
status: 'blocked',
|
||||
revision: 5,
|
||||
attempt: { id: attempt.id, runRecovery: recoveredAttempt.runRecovery },
|
||||
});
|
||||
expect(updated?.attempts).toEqual([recoveredAttempt]);
|
||||
});
|
||||
|
||||
it('rejects an active-attempt transition for a stale owner', async () => {
|
||||
const task = baseTask();
|
||||
const activeAttempt: TaskAttempt = {
|
||||
id: 'attempt_active',
|
||||
agent: 'codex',
|
||||
provider: 'codex-cli',
|
||||
status: 'running',
|
||||
};
|
||||
const staleAttempt: TaskAttempt = {
|
||||
...activeAttempt,
|
||||
id: 'attempt_stale',
|
||||
};
|
||||
const activeTask = { ...task, attempt: activeAttempt, attempts: [activeAttempt] };
|
||||
const coordinator = new AttemptLifecycleCoordinator(
|
||||
new MemoryAttemptLifecycleStore(activeTask)
|
||||
);
|
||||
|
||||
await expect(
|
||||
coordinator.persistActiveAttempt({ task: activeTask, attempt: staleAttempt })
|
||||
).rejects.toMatchObject({
|
||||
statusCode: 409,
|
||||
details: expect.objectContaining({
|
||||
activeAttemptId: activeAttempt.id,
|
||||
requestedAttemptId: staleAttempt.id,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('begins a new running attempt while archiving the displaced attempt', async () => {
|
||||
const task = baseTask();
|
||||
const previousAttempt: TaskAttempt = {
|
||||
id: 'attempt_previous',
|
||||
agent: 'hermes',
|
||||
provider: 'hermes-cli',
|
||||
status: 'complete',
|
||||
};
|
||||
const nextAttempt: TaskAttempt = {
|
||||
id: 'attempt_next',
|
||||
agent: 'codex',
|
||||
provider: 'codex-cli',
|
||||
status: 'running',
|
||||
};
|
||||
const queuedTask: Task = {
|
||||
...task,
|
||||
status: 'todo',
|
||||
attempt: previousAttempt,
|
||||
attempts: [],
|
||||
};
|
||||
const coordinator = new AttemptLifecycleCoordinator(
|
||||
new MemoryAttemptLifecycleStore(queuedTask)
|
||||
);
|
||||
|
||||
const updated = await coordinator.beginAttempt({
|
||||
task: queuedTask,
|
||||
attempt: nextAttempt,
|
||||
});
|
||||
|
||||
expect(updated).toMatchObject({
|
||||
status: 'in-progress',
|
||||
attempt: nextAttempt,
|
||||
attempts: [previousAttempt],
|
||||
});
|
||||
});
|
||||
|
||||
it('records launch failure without losing the displaced attempt', async () => {
|
||||
const task = baseTask();
|
||||
const previousAttempt: TaskAttempt = {
|
||||
id: 'attempt_previous',
|
||||
agent: 'hermes',
|
||||
provider: 'hermes-cli',
|
||||
status: 'complete',
|
||||
};
|
||||
const failedAttempt: TaskAttempt = {
|
||||
id: 'attempt_failed',
|
||||
agent: 'codex',
|
||||
provider: 'codex-cli',
|
||||
status: 'failed',
|
||||
ended: completedAt,
|
||||
};
|
||||
const launchingTask = {
|
||||
...task,
|
||||
attempt: { ...failedAttempt, status: 'running' as const, ended: undefined },
|
||||
attempts: [previousAttempt],
|
||||
};
|
||||
const coordinator = new AttemptLifecycleCoordinator(
|
||||
new MemoryAttemptLifecycleStore(launchingTask)
|
||||
);
|
||||
|
||||
const updated = await coordinator.persistLaunchFailure(task.id, failedAttempt);
|
||||
|
||||
expect(updated).toMatchObject({ status: 'todo', attempt: failedAttempt });
|
||||
expect(updated?.attempts).toEqual([previousAttempt, failedAttempt]);
|
||||
});
|
||||
|
||||
it('patches only the attempt that still owns the task', async () => {
|
||||
const task = baseTask();
|
||||
const attempt: TaskAttempt = {
|
||||
id: 'attempt_active',
|
||||
agent: 'codex',
|
||||
provider: 'codex-cli',
|
||||
status: 'running',
|
||||
};
|
||||
const activeTask = { ...task, attempt, attempts: [attempt] };
|
||||
const coordinator = new AttemptLifecycleCoordinator(
|
||||
new MemoryAttemptLifecycleStore(activeTask)
|
||||
);
|
||||
|
||||
await expect(
|
||||
coordinator.patchActiveAttempt(task.id, attempt.id, { sessionKey: 'session-1' })
|
||||
).resolves.toMatchObject({ attempt: { id: attempt.id, sessionKey: 'session-1' } });
|
||||
await expect(
|
||||
coordinator.patchActiveAttempt(task.id, 'attempt_stale', { sessionKey: 'session-2' })
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('persists terminal completion through the lifecycle seam', async () => {
|
||||
const { task, attempt, completionResult } = await completionFixture();
|
||||
const store = new MemoryAttemptLifecycleStore(task);
|
||||
const coordinator = new AttemptLifecycleCoordinator(store);
|
||||
|
||||
const outcome = await coordinator.persistCompletion({
|
||||
task,
|
||||
attempt,
|
||||
completionResult,
|
||||
});
|
||||
|
||||
expect(outcome.duplicate).toBe(false);
|
||||
expect(outcome.task).toMatchObject({
|
||||
status: 'done',
|
||||
revision: 5,
|
||||
attempt: {
|
||||
id: attempt.id,
|
||||
status: 'complete',
|
||||
ended: completedAt,
|
||||
completionResult: { idempotencyKey: completionResult.idempotencyKey },
|
||||
},
|
||||
});
|
||||
expect(outcome.task.attempts).toEqual([
|
||||
expect.objectContaining({ id: attempt.id, status: 'complete' }),
|
||||
]);
|
||||
await expect(store.getTask(task.id)).resolves.toEqual(outcome.task);
|
||||
});
|
||||
|
||||
it('retries a revision conflict against the same immutable attempt', async () => {
|
||||
const fixture = await completionFixture();
|
||||
const historicalAttempt: TaskAttempt = {
|
||||
id: 'attempt_historical',
|
||||
agent: 'hermes',
|
||||
provider: 'hermes-cli',
|
||||
status: 'complete',
|
||||
ended: '2026-08-24T03:00:00.000Z',
|
||||
};
|
||||
const currentTask: Task = {
|
||||
...fixture.task,
|
||||
revision: 5,
|
||||
attempts: [historicalAttempt, fixture.attempt],
|
||||
};
|
||||
const coordinator = new AttemptLifecycleCoordinator(
|
||||
new MemoryAttemptLifecycleStore(currentTask)
|
||||
);
|
||||
|
||||
const outcome = await coordinator.persistCompletion(fixture);
|
||||
|
||||
expect(outcome.task.revision).toBe(6);
|
||||
expect(outcome.task.attempts?.map((attempt) => attempt.id)).toEqual([
|
||||
historicalAttempt.id,
|
||||
fixture.attempt.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('treats the same persisted terminal result as an idempotent duplicate', async () => {
|
||||
const fixture = await completionFixture();
|
||||
const completedAttempt: TaskAttempt = {
|
||||
...fixture.attempt,
|
||||
status: 'complete',
|
||||
ended: completedAt,
|
||||
completionResult: fixture.completionResult,
|
||||
};
|
||||
const persistedTask: Task = {
|
||||
...fixture.task,
|
||||
status: 'done',
|
||||
revision: 5,
|
||||
attempt: completedAttempt,
|
||||
attempts: [completedAttempt],
|
||||
};
|
||||
const coordinator = new AttemptLifecycleCoordinator(
|
||||
new MemoryAttemptLifecycleStore(persistedTask)
|
||||
);
|
||||
|
||||
const outcome = await coordinator.persistCompletion(fixture);
|
||||
|
||||
expect(outcome.duplicate).toBe(true);
|
||||
expect(outcome.task).toEqual(persistedTask);
|
||||
});
|
||||
|
||||
it('fails closed when another attempt owns the task during retry', async () => {
|
||||
const fixture = await completionFixture();
|
||||
const competingAttempt: TaskAttempt = {
|
||||
id: 'attempt_competing',
|
||||
agent: 'hermes',
|
||||
provider: 'hermes-cli',
|
||||
status: 'running',
|
||||
};
|
||||
const coordinator = new AttemptLifecycleCoordinator(
|
||||
new MemoryAttemptLifecycleStore({
|
||||
...fixture.task,
|
||||
revision: 5,
|
||||
attempt: competingAttempt,
|
||||
attempts: [fixture.attempt, competingAttempt],
|
||||
})
|
||||
);
|
||||
|
||||
await expect(coordinator.persistCompletion(fixture)).rejects.toMatchObject({
|
||||
statusCode: 409,
|
||||
code: 'CONFLICT',
|
||||
details: expect.objectContaining({
|
||||
activeAttemptId: competingAttempt.id,
|
||||
finalizationAttemptId: fixture.attempt.id,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects stale completion input before the first persistence attempt', async () => {
|
||||
const fixture = await completionFixture();
|
||||
const competingAttempt: TaskAttempt = {
|
||||
id: 'attempt_competing',
|
||||
agent: 'hermes',
|
||||
provider: 'hermes-cli',
|
||||
status: 'running',
|
||||
};
|
||||
const staleTask: Task = {
|
||||
...fixture.task,
|
||||
attempt: competingAttempt,
|
||||
attempts: [fixture.attempt, competingAttempt],
|
||||
};
|
||||
const coordinator = new AttemptLifecycleCoordinator(new MemoryAttemptLifecycleStore(staleTask));
|
||||
|
||||
await expect(
|
||||
coordinator.persistCompletion({ ...fixture, task: staleTask })
|
||||
).rejects.toMatchObject({
|
||||
statusCode: 409,
|
||||
code: 'CONFLICT',
|
||||
details: expect.objectContaining({
|
||||
activeAttemptId: competingAttempt.id,
|
||||
finalizationAttemptId: fixture.attempt.id,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a different persisted terminal result for the same attempt', async () => {
|
||||
const fixture = await completionFixture();
|
||||
const competingFixture = await completionFixture('A different terminal claim completed.');
|
||||
const competingResult = competingFixture.completionResult;
|
||||
const completedAttempt: TaskAttempt = {
|
||||
...fixture.attempt,
|
||||
status: 'complete',
|
||||
ended: completedAt,
|
||||
completionResult: competingResult,
|
||||
};
|
||||
const persistedTask: Task = {
|
||||
...fixture.task,
|
||||
status: 'done',
|
||||
revision: 5,
|
||||
attempt: completedAttempt,
|
||||
attempts: [completedAttempt],
|
||||
};
|
||||
const coordinator = new AttemptLifecycleCoordinator(
|
||||
new MemoryAttemptLifecycleStore(persistedTask)
|
||||
);
|
||||
|
||||
await expect(coordinator.persistCompletion(fixture)).rejects.toMatchObject({
|
||||
statusCode: 409,
|
||||
code: 'CONFLICT',
|
||||
details: expect.objectContaining({
|
||||
persistedIdempotencyKey: competingResult.idempotencyKey,
|
||||
completionIdempotencyKey: fixture.completionResult.idempotencyKey,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves a non-active task status during startup reconciliation', async () => {
|
||||
const fixture = await completionFixture();
|
||||
const blockedTask: Task = { ...fixture.task, status: 'blocked' };
|
||||
const coordinator = new AttemptLifecycleCoordinator(
|
||||
new MemoryAttemptLifecycleStore(blockedTask)
|
||||
);
|
||||
|
||||
const outcome = await coordinator.persistCompletion({
|
||||
...fixture,
|
||||
task: blockedTask,
|
||||
preserveNonActiveTaskStatus: true,
|
||||
});
|
||||
|
||||
expect(outcome.task.status).toBe('blocked');
|
||||
});
|
||||
});
|
||||
152
server/src/__tests__/broadcast-storage-service.test.ts
Normal file
152
server/src/__tests__/broadcast-storage-service.test.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { Broadcast } from '@veritas-kanban/shared';
|
||||
import { BroadcastStorageService } from '../services/broadcast-storage-service.js';
|
||||
import { FileBroadcastRepository } from '../storage/broadcast-repository.js';
|
||||
|
||||
function broadcast(id: string, overrides: Partial<Broadcast> = {}): Broadcast {
|
||||
return {
|
||||
id,
|
||||
message: `Message ${id}`,
|
||||
priority: 'info',
|
||||
tags: [],
|
||||
createdAt: '2026-08-23T20:00:00.000Z',
|
||||
readBy: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('BroadcastStorageService', () => {
|
||||
let root: string;
|
||||
let broadcastsDir: string;
|
||||
let repository: FileBroadcastRepository;
|
||||
let service: BroadcastStorageService;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(path.join(process.cwd(), '.veritas-broadcast-storage-'));
|
||||
broadcastsDir = path.join(root, 'broadcasts');
|
||||
repository = new FileBroadcastRepository(broadcastsDir);
|
||||
service = new BroadcastStorageService({ repository });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('creates and round-trips safe frontmatter without field injection', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-08-23T21:00:00.000Z'));
|
||||
|
||||
const created = await service.create({
|
||||
message: 'Ship the release.',
|
||||
from: 'agent\npriority: urgent',
|
||||
tags: ['release'],
|
||||
});
|
||||
const loaded = await service.getById(created.id);
|
||||
|
||||
expect(loaded).toMatchObject({
|
||||
id: created.id,
|
||||
message: 'Ship the release.',
|
||||
priority: 'info',
|
||||
from: 'agent\npriority: urgent',
|
||||
tags: ['release'],
|
||||
createdAt: '2026-08-23T21:00:00.000Z',
|
||||
});
|
||||
expect(await readFile(path.join(broadcastsDir, `${created.id}.md`), 'utf8')).toContain(
|
||||
'from: "agent\\npriority: urgent"'
|
||||
);
|
||||
});
|
||||
|
||||
it('filters, sorts, limits, and marks broadcasts read without duplicate receipts', async () => {
|
||||
await repository.save(
|
||||
broadcast('older', {
|
||||
priority: 'urgent',
|
||||
createdAt: '2026-08-23T20:00:00.000Z',
|
||||
})
|
||||
);
|
||||
await repository.save(
|
||||
broadcast('newer', {
|
||||
priority: 'urgent',
|
||||
createdAt: '2026-08-23T21:00:00.000Z',
|
||||
})
|
||||
);
|
||||
await repository.save(
|
||||
broadcast('informational', {
|
||||
createdAt: '2026-08-23T22:00:00.000Z',
|
||||
})
|
||||
);
|
||||
|
||||
await expect(service.list({ priority: 'urgent', limit: 1 })).resolves.toMatchObject([
|
||||
{ id: 'newer' },
|
||||
]);
|
||||
await expect(
|
||||
service.list({ since: '2026-08-23T20:30:00.000Z', priority: 'urgent' })
|
||||
).resolves.toMatchObject([{ id: 'newer' }]);
|
||||
|
||||
await expect(service.markRead('newer', 'VERITAS')).resolves.toBe(true);
|
||||
await expect(service.markRead('newer', 'VERITAS')).resolves.toBe(true);
|
||||
await expect(service.list({ unread: true, agent: 'VERITAS' })).resolves.toMatchObject([
|
||||
{ id: 'informational' },
|
||||
{ id: 'older' },
|
||||
]);
|
||||
expect((await service.getById('newer'))?.readBy).toHaveLength(1);
|
||||
await expect(service.markRead('missing', 'VERITAS')).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('serializes concurrent read receipts', async () => {
|
||||
await repository.save(broadcast('shared'));
|
||||
await Promise.all([service.markRead('shared', 'TARS'), service.markRead('shared', 'CASE')]);
|
||||
|
||||
const receipts = (await repository.get('shared'))?.readBy.map(({ agent }) => agent);
|
||||
expect(receipts).toEqual(expect.arrayContaining(['TARS', 'CASE']));
|
||||
expect(receipts).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('loads legacy frontmatter and skips malformed broadcasts when listing', async () => {
|
||||
await mkdir(broadcastsDir, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(broadcastsDir, 'legacy.md'),
|
||||
['---', 'from: legacy-agent', 'tags: invalid', 'readBy: {}', 'ignored', '---', 'Legacy'].join(
|
||||
'\n'
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
await writeFile(path.join(broadcastsDir, 'broken.md'), 'not frontmatter', 'utf8');
|
||||
|
||||
await expect(service.getById('legacy')).resolves.toMatchObject({
|
||||
id: 'legacy',
|
||||
message: 'Legacy',
|
||||
priority: 'info',
|
||||
from: 'legacy-agent',
|
||||
tags: [],
|
||||
readBy: [],
|
||||
});
|
||||
await expect(service.getById('broken')).resolves.toBeNull();
|
||||
await expect(service.list()).resolves.toMatchObject([{ id: 'legacy' }]);
|
||||
});
|
||||
|
||||
it('fails closed for unsafe, non-file, oversized, and linked storage paths', async () => {
|
||||
await mkdir(broadcastsDir, { recursive: true });
|
||||
const outside = path.join(root, 'outside.md');
|
||||
await writeFile(outside, 'not a broadcast', 'utf8');
|
||||
await symlink(outside, path.join(broadcastsDir, 'linked.md'));
|
||||
await expect(service.getById('linked')).resolves.toBeNull();
|
||||
|
||||
await mkdir(path.join(broadcastsDir, 'directory.md'));
|
||||
await expect(service.getById('directory')).resolves.toBeNull();
|
||||
await expect(service.getById('../outside')).resolves.toBeNull();
|
||||
await expect(service.markRead('../outside', 'VERITAS')).resolves.toBe(false);
|
||||
await expect(service.create({ message: 'x'.repeat(1024 * 1024) })).rejects.toThrow(
|
||||
/Failed to create broadcast/
|
||||
);
|
||||
|
||||
const realDirectory = path.join(root, 'real-broadcasts');
|
||||
const linkedDirectory = path.join(root, 'linked-broadcasts');
|
||||
await mkdir(realDirectory);
|
||||
await symlink(realDirectory, linkedDirectory, 'dir');
|
||||
const linkedService = new BroadcastStorageService({ broadcastsDir: linkedDirectory });
|
||||
await expect(linkedService.list()).rejects.toThrow(/Failed to list broadcasts/);
|
||||
});
|
||||
});
|
||||
171
server/src/__tests__/ceremony-state-repository.test.ts
Normal file
171
server/src/__tests__/ceremony-state-repository.test.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { lstat, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { CeremonyRequirement } from '@veritas-kanban/shared';
|
||||
import { CeremonyService } from '../services/ceremony-service.js';
|
||||
import {
|
||||
FileCeremonyStateRepository,
|
||||
InMemoryCeremonyStateRepository,
|
||||
} from '../storage/ceremony-state-repository.js';
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
||||
return { ...actual, lstat: vi.fn(actual.lstat) };
|
||||
});
|
||||
|
||||
function requirement(id: string): CeremonyRequirement {
|
||||
return {
|
||||
id,
|
||||
kind: 'design_review',
|
||||
status: 'pending',
|
||||
enforcementMode: 'warn',
|
||||
title: `Review ${id}`,
|
||||
reason: 'Test requirement',
|
||||
target: { taskId: `task-${id}` },
|
||||
trigger: 'task.completion',
|
||||
participants: [],
|
||||
requiredArtifacts: [],
|
||||
artifacts: [],
|
||||
actionItems: [],
|
||||
createdAt: '2026-08-23T20:00:00.000Z',
|
||||
updatedAt: '2026-08-23T20:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
describe('FileCeremonyStateRepository', () => {
|
||||
let root: string;
|
||||
let storageDir: string;
|
||||
let repository: FileCeremonyStateRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(path.join(process.cwd(), '.veritas-ceremony-state-'));
|
||||
storageDir = path.join(root, 'ceremonies');
|
||||
repository = new FileCeremonyStateRepository(storageDir);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('reads defaults and persists normalized state', async () => {
|
||||
await expect(repository.read()).resolves.toMatchObject({ version: 1, requirements: [] });
|
||||
await repository.update((state) => ({
|
||||
...state,
|
||||
requirements: [requirement('one')],
|
||||
}));
|
||||
await expect(repository.read()).resolves.toMatchObject({
|
||||
version: 1,
|
||||
requirements: [requirement('one')],
|
||||
});
|
||||
|
||||
await writeFile(
|
||||
path.join(storageDir, 'requirements.json'),
|
||||
JSON.stringify({ version: 99, requirements: 'legacy' }),
|
||||
'utf8'
|
||||
);
|
||||
await expect(repository.read()).resolves.toMatchObject({ version: 1, requirements: [] });
|
||||
});
|
||||
|
||||
it('serializes concurrent read-modify-write updates', async () => {
|
||||
await Promise.all([
|
||||
repository.update((state) => ({
|
||||
...state,
|
||||
requirements: [...state.requirements, requirement('one')],
|
||||
})),
|
||||
repository.update((state) => ({
|
||||
...state,
|
||||
requirements: [...state.requirements, requirement('two')],
|
||||
})),
|
||||
]);
|
||||
|
||||
expect((await repository.read()).requirements.map(({ id }) => id)).toEqual(
|
||||
expect.arrayContaining(['one', 'two'])
|
||||
);
|
||||
});
|
||||
|
||||
it('prevents duplicate requirements across concurrent service instances', async () => {
|
||||
const serviceOptions = {
|
||||
storageDir,
|
||||
persist: true,
|
||||
audit: vi.fn().mockResolvedValue(undefined),
|
||||
governanceTraceService: { record: vi.fn().mockResolvedValue({ id: 'trace-one' }) } as never,
|
||||
};
|
||||
const first = new CeremonyService(serviceOptions);
|
||||
const second = new CeremonyService(serviceOptions);
|
||||
const input = {
|
||||
kind: 'design_review' as const,
|
||||
enforcementMode: 'warn' as const,
|
||||
reason: 'Concurrent creation test',
|
||||
target: { taskId: 'task-concurrent' },
|
||||
trigger: 'task.completion' as const,
|
||||
};
|
||||
|
||||
const [firstResult, secondResult] = await Promise.all([
|
||||
first.create(input),
|
||||
second.create(input),
|
||||
]);
|
||||
|
||||
expect(firstResult.id).toBe(secondResult.id);
|
||||
await expect(repository.read()).resolves.toMatchObject({
|
||||
requirements: [{ id: firstResult.id }],
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects symbolic links and non-file state paths', async () => {
|
||||
await mkdir(storageDir, { recursive: true });
|
||||
const target = path.join(root, 'outside.json');
|
||||
await writeFile(target, JSON.stringify({ version: 1, requirements: [] }), 'utf8');
|
||||
await symlink(target, path.join(storageDir, 'requirements.json'));
|
||||
await expect(repository.read()).rejects.toThrow(/symbolic link/i);
|
||||
|
||||
await rm(path.join(storageDir, 'requirements.json'));
|
||||
await mkdir(path.join(storageDir, 'requirements.json'));
|
||||
await expect(repository.read()).rejects.toThrow(/bounded regular file/i);
|
||||
});
|
||||
|
||||
it('rejects state replaced after its file handle is opened', async () => {
|
||||
await repository.update((state) => ({ ...state, requirements: [requirement('one')] }));
|
||||
const actual = await vi.importActual<typeof import('node:fs/promises')>('node:fs/promises');
|
||||
vi.mocked(lstat).mockImplementationOnce(async (filePath) => {
|
||||
const stats = await actual.lstat(filePath);
|
||||
return Object.assign(Object.create(Object.getPrototypeOf(stats)), stats, {
|
||||
ino: stats.ino + 1,
|
||||
});
|
||||
});
|
||||
|
||||
await expect(repository.read()).rejects.toThrow(/changed file/i);
|
||||
});
|
||||
|
||||
it('rejects symbolic-link directories and oversized state', async () => {
|
||||
const realDirectory = path.join(root, 'real-ceremonies');
|
||||
const linkedDirectory = path.join(root, 'linked-ceremonies');
|
||||
await mkdir(realDirectory);
|
||||
await symlink(realDirectory, linkedDirectory, 'dir');
|
||||
const linkedRepository = new FileCeremonyStateRepository(linkedDirectory);
|
||||
await expect(
|
||||
linkedRepository.update((state) => ({ ...state, requirements: [requirement('unsafe')] }))
|
||||
).rejects.toThrow(/regular directory/i);
|
||||
|
||||
await expect(
|
||||
repository.update((state) => ({
|
||||
...state,
|
||||
requirements: [{ ...requirement('large'), reason: 'x'.repeat(16 * 1024 * 1024) }],
|
||||
}))
|
||||
).rejects.toThrow(/16 MiB/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('InMemoryCeremonyStateRepository', () => {
|
||||
it('reads and updates transient ceremony state', async () => {
|
||||
const repository = new InMemoryCeremonyStateRepository();
|
||||
await repository.update((state) => ({
|
||||
...state,
|
||||
requirements: [requirement('memory')],
|
||||
}));
|
||||
|
||||
await expect(repository.read()).resolves.toMatchObject({
|
||||
version: 1,
|
||||
requirements: [requirement('memory')],
|
||||
});
|
||||
});
|
||||
});
|
||||
272
server/src/__tests__/chat-repository.test.ts
Normal file
272
server/src/__tests__/chat-repository.test.ts
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mkdir, mkdtemp, readFile, rm, symlink, truncate, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { ChatMessage, ChatSession, SquadMessage } from '@veritas-kanban/shared';
|
||||
import { FileChatRepository } from '../storage/chat-repository.js';
|
||||
import { SqliteChatRepository } from '../storage/sqlite/chat-repository.js';
|
||||
|
||||
function session(id: string, updated = '2026-08-23T00:00:00.000Z'): ChatSession {
|
||||
return {
|
||||
id,
|
||||
title: 'Session',
|
||||
messages: [],
|
||||
agent: 'VERITAS',
|
||||
mode: 'ask',
|
||||
created: '2026-08-22T00:00:00.000Z',
|
||||
updated,
|
||||
};
|
||||
}
|
||||
|
||||
function message(id: string, content: string): ChatMessage {
|
||||
return {
|
||||
id,
|
||||
role: 'assistant',
|
||||
content,
|
||||
timestamp: `2026-08-23T00:00:0${id.endsWith('2') ? '2' : '1'}.000Z`,
|
||||
agent: 'VERITAS',
|
||||
model: 'gpt',
|
||||
};
|
||||
}
|
||||
|
||||
describe('FileChatRepository', () => {
|
||||
let root: string;
|
||||
let chatsDir: string;
|
||||
let repository: FileChatRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(path.join(process.cwd(), '.veritas-chat-repository-'));
|
||||
chatsDir = path.join(root, 'chats');
|
||||
repository = new FileChatRepository(chatsDir);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('round-trips board and task sessions with concurrent atomic appends', async () => {
|
||||
await expect(repository.getSession('chat_missing')).resolves.toBeNull();
|
||||
await repository.saveSession(session('chat_old'));
|
||||
await repository.saveSession(session('chat_new', '2026-08-24T00:00:00.000Z'));
|
||||
await repository.saveSession({ ...session('task_123'), taskId: '123' });
|
||||
|
||||
await expect(
|
||||
Promise.all([
|
||||
repository.appendSessionMessage('chat_new', message('msg_1', 'one')),
|
||||
repository.appendSessionMessage('chat_new', message('msg_2', 'two')),
|
||||
])
|
||||
).resolves.toEqual([true, true]);
|
||||
await expect(
|
||||
repository.appendSessionMessage('chat_missing', message('msg_3', 'x'))
|
||||
).resolves.toBe(false);
|
||||
|
||||
const stored = await repository.getSession('chat_new');
|
||||
expect(stored?.messages.map((entry) => entry.content).sort()).toEqual(['one', 'two']);
|
||||
const restarted = new FileChatRepository(chatsDir);
|
||||
await expect(restarted.getSession('chat_new')).resolves.toMatchObject({
|
||||
messages: expect.arrayContaining([
|
||||
expect.objectContaining({ content: 'one' }),
|
||||
expect.objectContaining({ content: 'two' }),
|
||||
]),
|
||||
});
|
||||
await expect(repository.getSessionForTask('123')).resolves.toMatchObject({ id: 'task_123' });
|
||||
|
||||
await mkdir(path.join(chatsDir, 'sessions', 'nested.md'));
|
||||
await writeFile(path.join(chatsDir, 'sessions', 'ignored.txt'), 'ignored', 'utf8');
|
||||
await symlink(
|
||||
path.join(chatsDir, 'sessions', 'chat_old.md'),
|
||||
path.join(chatsDir, 'sessions', 'alias.md')
|
||||
);
|
||||
await expect(repository.listBoardSessions()).resolves.toMatchObject([
|
||||
{ id: 'chat_new' },
|
||||
{ id: 'chat_old' },
|
||||
]);
|
||||
|
||||
const malformedPath = path.join(chatsDir, 'sessions', 'chat_old.md');
|
||||
await writeFile(
|
||||
malformedPath,
|
||||
`${await readFile(malformedPath, 'utf8')}\n---\nnot a message header\nignored\n`,
|
||||
'utf8'
|
||||
);
|
||||
await expect(repository.getSession('chat_old')).resolves.toMatchObject({ messages: [] });
|
||||
|
||||
await expect(repository.deleteSession('chat_new')).resolves.toBe(true);
|
||||
await expect(repository.deleteSession('chat_new')).resolves.toBe(false);
|
||||
|
||||
await mkdir(path.join(chatsDir, 'sessions', 'chat_directory.md'));
|
||||
await expect(repository.deleteSession('chat_directory')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('round-trips squad logs, legacy formatting, filters, and metadata', async () => {
|
||||
const first: SquadMessage = {
|
||||
id: 'msg_first',
|
||||
agent: 'TARS',
|
||||
displayName: 'Tars',
|
||||
message: 'First',
|
||||
tags: ['testing', 'chat'],
|
||||
timestamp: '2026-08-23T00:00:00.000Z',
|
||||
model: 'gpt',
|
||||
system: true,
|
||||
event: 'agent.status',
|
||||
taskTitle: 'Task A',
|
||||
duration: '5s',
|
||||
};
|
||||
const second: SquadMessage = {
|
||||
id: 'msg_second',
|
||||
agent: 'CASE',
|
||||
message: 'Second',
|
||||
timestamp: '2026-08-24T00:00:00.000Z',
|
||||
duration: '2s',
|
||||
};
|
||||
await repository.appendSquadMessage(first);
|
||||
await repository.appendSquadMessage(second);
|
||||
expect(await readFile(path.join(chatsDir, 'squad', '2026-08-23.md'), 'utf8')).toContain(
|
||||
'msg_first'
|
||||
);
|
||||
|
||||
expect(await repository.listSquadMessages()).toMatchObject([first, second]);
|
||||
expect(await repository.listSquadMessages({ includeSystem: false })).toMatchObject([second]);
|
||||
expect(await repository.listSquadMessages({ agent: 'CASE' })).toMatchObject([second]);
|
||||
expect(
|
||||
await repository.listSquadMessages({ since: '2026-08-23T12:00:00.000Z', limit: 1 })
|
||||
).toMatchObject([second]);
|
||||
|
||||
await expect(repository.readSquadMetadata()).resolves.toMatchObject({
|
||||
version: 1,
|
||||
messages: {},
|
||||
reads: {},
|
||||
});
|
||||
await expect(
|
||||
repository.updateSquadMetadata((metadata) => {
|
||||
metadata.messages.msg_first = { pinned: true };
|
||||
return 'updated';
|
||||
})
|
||||
).resolves.toBe('updated');
|
||||
await expect(repository.readSquadMetadata()).resolves.toMatchObject({
|
||||
messages: { msg_first: { pinned: true } },
|
||||
});
|
||||
|
||||
await writeFile(path.join(chatsDir, 'squad', 'metadata.json'), '{}', 'utf8');
|
||||
await expect(repository.readSquadMetadata()).resolves.toMatchObject({
|
||||
messages: {},
|
||||
reads: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('skips missing and malformed squad log entries', async () => {
|
||||
await repository.listSquadMessages();
|
||||
const squadPath = path.join(chatsDir, 'squad', '2026-08-23.md');
|
||||
await writeFile(
|
||||
squadPath,
|
||||
[
|
||||
'# Squad Chat - 2026-08-23',
|
||||
'',
|
||||
'## only | two',
|
||||
'',
|
||||
'ignored',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## | msg_missing_agent | 2026-08-23T00:00:00.000Z',
|
||||
'',
|
||||
'ignored',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## CASE | msg_missing_timestamp | [system]',
|
||||
'',
|
||||
'ignored',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
await expect(repository.listSquadMessages()).resolves.toEqual([]);
|
||||
|
||||
const internals = repository as unknown as {
|
||||
readOptionalBoundedFile: () => Promise<null>;
|
||||
};
|
||||
internals.readOptionalBoundedFile = vi.fn(async () => null);
|
||||
await expect(repository.listSquadMessages()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects traversal, symbolic links, oversized sessions, and invalid squad timestamps', async () => {
|
||||
await expect(repository.getSession('../outside')).rejects.toThrow();
|
||||
await expect(repository.getSessionForTask('../outside')).rejects.toThrow();
|
||||
await expect(
|
||||
repository.saveSession({ ...session('chat_large'), title: 'x'.repeat(16 * 1024 * 1024) })
|
||||
).rejects.toThrow(/storage limit/);
|
||||
|
||||
const oversizedPath = path.join(chatsDir, 'sessions', 'chat_oversized.md');
|
||||
await writeFile(oversizedPath, '', 'utf8');
|
||||
await truncate(oversizedPath, 16 * 1024 * 1024 + 1);
|
||||
await expect(repository.getSession('chat_oversized')).rejects.toThrow(/bounded regular file/);
|
||||
await expect(repository.getSession('x'.repeat(256))).rejects.toThrow();
|
||||
await expect(
|
||||
(
|
||||
repository as unknown as {
|
||||
readOptionalBoundedFile: (
|
||||
filePath: string,
|
||||
maximumBytes: number,
|
||||
label: string
|
||||
) => Promise<string | null>;
|
||||
}
|
||||
).readOptionalBoundedFile(path.join(root, 'outside.md'), 1024, 'Chat session')
|
||||
).rejects.toThrow(/outside its repository/);
|
||||
await expect(
|
||||
repository.appendSquadMessage({
|
||||
id: 'msg_invalid',
|
||||
agent: 'CASE',
|
||||
message: 'invalid',
|
||||
timestamp: 'invalid',
|
||||
})
|
||||
).rejects.toThrow(/timestamp is invalid/);
|
||||
|
||||
const outside = path.join(root, 'outside.md');
|
||||
await writeFile(outside, 'outside', 'utf8');
|
||||
const sessionPath = path.join(chatsDir, 'sessions', 'chat_link.md');
|
||||
await symlink(outside, sessionPath);
|
||||
await expect(repository.getSession('chat_link')).rejects.toThrow(/symbolic link/i);
|
||||
|
||||
const linkedRoot = path.join(root, 'linked-root');
|
||||
const target = path.join(root, 'target');
|
||||
await mkdir(target);
|
||||
await symlink(target, linkedRoot);
|
||||
const linkedRepository = new FileChatRepository(linkedRoot);
|
||||
await expect(linkedRepository.listBoardSessions()).rejects.toThrow(/regular directories/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SqliteChatRepository append transaction outcomes', () => {
|
||||
it('commits a missing-session no-op and rolls back malformed stored state', () => {
|
||||
const missingExec = vi.fn();
|
||||
const missing = new SqliteChatRepository({
|
||||
getConnection: () => ({
|
||||
exec: missingExec,
|
||||
prepare: () => ({ get: () => undefined }),
|
||||
}),
|
||||
} as never);
|
||||
|
||||
expect(missing.appendSessionMessage('missing', message('msg_1', 'missing'))).toBe(false);
|
||||
expect(missingExec.mock.calls.map(([statement]) => statement)).toEqual([
|
||||
'BEGIN IMMEDIATE;',
|
||||
'COMMIT;',
|
||||
]);
|
||||
|
||||
const malformedExec = vi.fn();
|
||||
const malformed = new SqliteChatRepository({
|
||||
getConnection: () => ({
|
||||
exec: malformedExec,
|
||||
prepare: () => ({ get: () => ({ id: 'chat_broken', session_json: '{' }) }),
|
||||
}),
|
||||
} as never);
|
||||
|
||||
expect(() =>
|
||||
malformed.appendSessionMessage('chat_broken', message('msg_2', 'broken'))
|
||||
).toThrow();
|
||||
expect(malformedExec.mock.calls.map(([statement]) => statement)).toEqual([
|
||||
'BEGIN IMMEDIATE;',
|
||||
'ROLLBACK;',
|
||||
]);
|
||||
});
|
||||
});
|
||||
85
server/src/__tests__/codex-event-interpreter.test.ts
Normal file
85
server/src/__tests__/codex-event-interpreter.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
interpretCodexEvent,
|
||||
redactProviderTraceText,
|
||||
} from '../services/codex-event-interpreter.js';
|
||||
|
||||
describe('Codex event interpretation', () => {
|
||||
it('normalizes nested completion events and protects trace text', () => {
|
||||
const interpreted = interpretCodexEvent(
|
||||
{
|
||||
args: [' status ', '', 42],
|
||||
file: 'direct.ts',
|
||||
file_path: [
|
||||
'./result.ts',
|
||||
'../parent.ts',
|
||||
'/tmp/output.log',
|
||||
'https://example.com/result',
|
||||
'nested/result.json',
|
||||
'notes.md',
|
||||
'line\nbreak',
|
||||
'',
|
||||
42,
|
||||
],
|
||||
retryAttempt: '2',
|
||||
retryDelayMs: false,
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 4,
|
||||
cost_usd: 0.01,
|
||||
model: 'gpt-test',
|
||||
},
|
||||
nested: { final_response: ' done ' },
|
||||
item: { type: 'completed_item' },
|
||||
},
|
||||
'turn.completed'
|
||||
);
|
||||
|
||||
expect(interpreted).toMatchObject({
|
||||
command: 'status',
|
||||
files: [
|
||||
'direct.ts',
|
||||
'./result.ts',
|
||||
'../parent.ts',
|
||||
'/tmp/output.log',
|
||||
'https://example.com/result',
|
||||
'nested/result.json',
|
||||
'notes.md',
|
||||
],
|
||||
retryAttempt: 2,
|
||||
tool: 'completed_item',
|
||||
traceStepType: 'complete',
|
||||
logActivity: true,
|
||||
summary: 'done',
|
||||
usage: {
|
||||
inputTokens: 10,
|
||||
outputTokens: 4,
|
||||
totalTokens: 14,
|
||||
cost: 0.01,
|
||||
model: 'gpt-test',
|
||||
},
|
||||
});
|
||||
expect(interpreted.stream).toBeUndefined();
|
||||
|
||||
const redacted = redactProviderTraceText(`token=secret-value ${'x'.repeat(2100)}`);
|
||||
expect(redacted.startsWith('token=[REDACTED]')).toBe(true);
|
||||
expect(redacted).toHaveLength(2003);
|
||||
expect(redacted.endsWith('...')).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['turn.retrying', {}, 'retry', undefined],
|
||||
['run.aborted', {}, 'abort', undefined],
|
||||
['run.cancelled', {}, 'abort', undefined],
|
||||
['turn.failed', { message: 'failed' }, 'error', undefined],
|
||||
['error', { error: 'broken' }, 'error', 'stderr'],
|
||||
['run.finalizing', {}, 'finalize', undefined],
|
||||
['response.output', {}, 'stream', 'stdout'],
|
||||
['item.created', { item: { type: 'message_delta' } }, 'stream', undefined],
|
||||
['response.completed', {}, 'complete', undefined],
|
||||
['item.created', {}, 'execute', undefined],
|
||||
] as const)('maps %s to its trace lifecycle', (type, event, traceStepType, stream) => {
|
||||
expect(interpretCodexEvent(event, type)).toMatchObject({ traceStepType, stream });
|
||||
});
|
||||
});
|
||||
|
|
@ -213,7 +213,6 @@ import {
|
|||
import type { ThreadEvent } from '@openai/codex-sdk';
|
||||
import type {
|
||||
AgentConfig,
|
||||
RunLaunchRuntime,
|
||||
RunApprovalRequest,
|
||||
SandboxPolicyDryRunResult,
|
||||
Task,
|
||||
|
|
@ -229,6 +228,7 @@ import {
|
|||
type CompletionEvidenceSource,
|
||||
} from '../services/task-envelope-service.js';
|
||||
import { ProviderCompletionService } from '../services/provider-completion-service.js';
|
||||
import type { ReflectionExtractionJobService } from '../services/reflection-extraction-job-service.js';
|
||||
import type {
|
||||
CreateRunApprovalRequestInput,
|
||||
RunApprovalBrokerService,
|
||||
|
|
@ -246,30 +246,16 @@ import type { WorkspaceExecutionTrustService } from '../services/workspace-execu
|
|||
import type { AdmissionControlService } from '../services/admission-control-service.js';
|
||||
import type { RunTerminalService } from '../services/run-terminal-service.js';
|
||||
import type { WorkspaceCheckpointService } from '../services/workspace-checkpoint-service.js';
|
||||
import type { RunLaunchCompiler } from '../services/run-launch-compiler.js';
|
||||
|
||||
const fixtureDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'codex');
|
||||
|
||||
type TestableClawdbotAgentService = ClawdbotAgentService & {
|
||||
logsDir: string;
|
||||
buildRunLaunchEnvironment(
|
||||
provider: 'openclaw' | 'codex-sdk',
|
||||
sandboxPolicy: SandboxPolicyDryRunResult
|
||||
): Pick<RunLaunchRuntime, 'environmentKeys' | 'credentialReferences'>;
|
||||
buildRunLaunchRuntime(
|
||||
provider: 'openclaw' | 'codex-sdk',
|
||||
agentConfig: AgentConfig | undefined,
|
||||
taskId: string,
|
||||
logPath: string,
|
||||
attemptId: string,
|
||||
sandboxPolicy: SandboxPolicyDryRunResult
|
||||
): RunLaunchRuntime;
|
||||
normalizeRunLaunchTaskPrompt(
|
||||
prompt: string,
|
||||
attemptId: string,
|
||||
worktreePath: string | undefined,
|
||||
taskEnvelopeDigest: string,
|
||||
providerRuntimeDigest: string
|
||||
): string;
|
||||
runLaunchCompiler: Pick<
|
||||
RunLaunchCompiler,
|
||||
'buildRunLaunchEnvironment' | 'buildRunLaunchRuntime' | 'normalizeRunLaunchTaskPrompt'
|
||||
>;
|
||||
handleCodexEvent(
|
||||
event: Record<string, unknown>,
|
||||
logPath: string,
|
||||
|
|
@ -283,6 +269,12 @@ type TestableClawdbotAgentService = ClawdbotAgentService & {
|
|||
recordCodexThread(task: Task, attemptId: string, threadId: string): Promise<void>;
|
||||
};
|
||||
|
||||
function testReflectionExtractionJobs(): Pick<ReflectionExtractionJobService, 'enqueue'> {
|
||||
return {
|
||||
enqueue: vi.fn().mockResolvedValue({}),
|
||||
} as unknown as Pick<ReflectionExtractionJobService, 'enqueue'>;
|
||||
}
|
||||
|
||||
function testableService(
|
||||
tmpDir: string,
|
||||
credentialLeases?: CredentialLeaseLifecycle,
|
||||
|
|
@ -332,7 +324,7 @@ function testableService(
|
|||
handleRunCompletion: mockHandleDurableGoalCompletion,
|
||||
reconcilePlannedForTask: mockReconcileDurableGoalContinuation,
|
||||
},
|
||||
undefined,
|
||||
testReflectionExtractionJobs(),
|
||||
undefined,
|
||||
runTerminals,
|
||||
workspaceCheckpoints
|
||||
|
|
@ -1699,6 +1691,15 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
await waitFor(() => {
|
||||
expect(received.some(({ method }) => method === 'turn/start')).toBe(true);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockPatchTaskAttempt).toHaveBeenCalledWith(
|
||||
task.id,
|
||||
status.attemptId,
|
||||
expect.objectContaining({
|
||||
conversation: expect.objectContaining({ currentTurnId: 'turn-app-server-fixture' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(captureBoundary).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
taskId: task.id,
|
||||
|
|
@ -2290,14 +2291,14 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
|
||||
it('normalizes checkpoint age out of material instruction evidence', () => {
|
||||
const service = testableService(tmpDir);
|
||||
const first = service.normalizeRunLaunchTaskPrompt(
|
||||
const first = service.runLaunchCompiler.normalizeRunLaunchTaskPrompt(
|
||||
`Envelope sha256:${'c'.repeat(64)} Last Checkpoint: 2026-07-23T20:00:00.000Z (5 minutes ago)`,
|
||||
'attempt-a',
|
||||
tmpDir,
|
||||
`sha256:${'c'.repeat(64)}`,
|
||||
`sha256:${'a'.repeat(64)}`
|
||||
);
|
||||
const later = service.normalizeRunLaunchTaskPrompt(
|
||||
const later = service.runLaunchCompiler.normalizeRunLaunchTaskPrompt(
|
||||
`Envelope sha256:${'d'.repeat(64)} Last Checkpoint: 2026-07-23T20:00:00.000Z (125 minutes ago)`,
|
||||
'attempt-b',
|
||||
tmpDir,
|
||||
|
|
@ -2318,7 +2319,7 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
vi.stubEnv('OPENCLAW_GATEWAY_ALLOW_PRIVATE', '');
|
||||
const service = testableService(tmpDir);
|
||||
|
||||
const environment = service.buildRunLaunchEnvironment(
|
||||
const environment = service.runLaunchCompiler.buildRunLaunchEnvironment(
|
||||
'openclaw',
|
||||
{} as SandboxPolicyDryRunResult
|
||||
);
|
||||
|
|
@ -2337,7 +2338,7 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
vi.stubEnv('OPENCLAW_GATEWAY_ALLOW_PRIVATE', '');
|
||||
const service = testableService(tmpDir);
|
||||
|
||||
const runtime = service.buildRunLaunchRuntime(
|
||||
const runtime = service.runLaunchCompiler.buildRunLaunchRuntime(
|
||||
'openclaw',
|
||||
{
|
||||
type: 'openclaw',
|
||||
|
|
@ -2374,7 +2375,7 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
vi.stubEnv('OPENCLAW_GATEWAY_ALLOW_PRIVATE', 'true');
|
||||
const service = testableService(tmpDir);
|
||||
|
||||
const runtime = service.buildRunLaunchRuntime(
|
||||
const runtime = service.runLaunchCompiler.buildRunLaunchRuntime(
|
||||
'openclaw',
|
||||
{
|
||||
type: 'openclaw',
|
||||
|
|
@ -2402,7 +2403,7 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
},
|
||||
} as SandboxPolicyDryRunResult;
|
||||
|
||||
const overridden = service.buildRunLaunchRuntime(
|
||||
const overridden = service.runLaunchCompiler.buildRunLaunchRuntime(
|
||||
'codex-sdk',
|
||||
{
|
||||
type: 'codex-sdk',
|
||||
|
|
@ -2415,7 +2416,7 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
'attempt-sdk',
|
||||
sandboxPolicy
|
||||
);
|
||||
const bundled = service.buildRunLaunchRuntime(
|
||||
const bundled = service.runLaunchCompiler.buildRunLaunchRuntime(
|
||||
'codex-sdk',
|
||||
{
|
||||
type: 'codex-sdk',
|
||||
|
|
@ -3438,6 +3439,39 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
await expect(service.getAgentStatus(task.id)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('isolates reflection enqueue failures from provider completion', async () => {
|
||||
const child = createControllableChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const reflectionExtractionJobs = {
|
||||
enqueue: vi.fn().mockRejectedValue(new Error('reflection queue unavailable')),
|
||||
} as unknown as Pick<ReflectionExtractionJobService, 'enqueue'>;
|
||||
const service = testableService(tmpDir);
|
||||
(
|
||||
service as unknown as {
|
||||
reflectionExtractionJobs: Pick<ReflectionExtractionJobService, 'enqueue'>;
|
||||
}
|
||||
).reflectionExtractionJobs = reflectionExtractionJobs;
|
||||
const active = await service.startAgent(task.id, 'codex');
|
||||
|
||||
await service.completeAgent(
|
||||
task.id,
|
||||
{ success: true, summary: 'Provider completion remains authoritative.' },
|
||||
{
|
||||
attemptId: active.attemptId,
|
||||
terminalSource: 'process',
|
||||
providerRuntimeManifestDigest: active.providerRuntimeManifest.digest,
|
||||
}
|
||||
);
|
||||
await waitFor(() => expect(reflectionExtractionJobs.enqueue).toHaveBeenCalledOnce());
|
||||
await Promise.resolve();
|
||||
|
||||
expect(task.attempt).toMatchObject({
|
||||
id: active.attemptId,
|
||||
status: 'complete',
|
||||
completionResult: { status: 'success' },
|
||||
});
|
||||
});
|
||||
|
||||
it('reports attempt-scoped terminals and cleans them up before completion commits', async () => {
|
||||
const child = createControllableChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
|
@ -3688,23 +3722,37 @@ describe('ClawdbotAgentService Codex providers', () => {
|
|||
...request,
|
||||
requestId: 'terminal-finalization-race',
|
||||
};
|
||||
const racingExecution = service.executeRunTerminal(
|
||||
task.id,
|
||||
active.attemptId,
|
||||
racingRequest
|
||||
);
|
||||
const racingExecution = service.executeRunTerminal(task.id, active.attemptId, racingRequest);
|
||||
await waitFor(() => expect(runTerminals.execute).toHaveBeenCalledTimes(2));
|
||||
const stopping = service.stopAgent(task.id, active.attemptId);
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
expect(runTerminals.cleanupAttempt).not.toHaveBeenCalled();
|
||||
|
||||
resolveLaunch?.();
|
||||
await expect(racingExecution).resolves.toMatchObject({
|
||||
status: 'started',
|
||||
handle: { id: 'terminal_finalization_race' },
|
||||
const originalProviderComplete = ProviderCompletionService.prototype.complete;
|
||||
let resolveProviderCompletion: (() => void) | undefined;
|
||||
const providerCompletion = new Promise<void>((resolve) => {
|
||||
resolveProviderCompletion = resolve;
|
||||
});
|
||||
await stopping;
|
||||
expect(runTerminals.cleanupAttempt).toHaveBeenCalledOnce();
|
||||
const completionSpy = vi
|
||||
.spyOn(ProviderCompletionService.prototype, 'complete')
|
||||
.mockImplementation(async function (input) {
|
||||
const result = await originalProviderComplete.call(this, input);
|
||||
resolveProviderCompletion?.();
|
||||
return result;
|
||||
});
|
||||
try {
|
||||
const stopping = service.stopAgent(task.id, active.attemptId);
|
||||
await providerCompletion;
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(runTerminals.cleanupAttempt).not.toHaveBeenCalled();
|
||||
|
||||
resolveLaunch?.();
|
||||
await expect(racingExecution).resolves.toMatchObject({
|
||||
status: 'started',
|
||||
handle: { id: 'terminal_finalization_race' },
|
||||
});
|
||||
await stopping;
|
||||
expect(runTerminals.cleanupAttempt).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
completionSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not let a competing terminal claim poison an in-flight finalizer', async () => {
|
||||
|
|
|
|||
89
server/src/__tests__/conflict-workspace-repository.test.ts
Normal file
89
server/src/__tests__/conflict-workspace-repository.test.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
chmod,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rm,
|
||||
stat,
|
||||
symlink,
|
||||
truncate,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { LocalConflictWorkspaceRepository } from '../storage/conflict-workspace-repository.js';
|
||||
|
||||
describe('LocalConflictWorkspaceRepository', () => {
|
||||
let root: string;
|
||||
let outsideRoot: string;
|
||||
const repository = new LocalConflictWorkspaceRepository();
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(path.join(process.cwd(), '.veritas-conflict-workspace-'));
|
||||
outsideRoot = await mkdtemp(path.join(process.cwd(), '.veritas-conflict-outside-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all([
|
||||
rm(root, { recursive: true, force: true }),
|
||||
rm(outsideRoot, { recursive: true, force: true }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('inspects Git state paths and distinguishes missing entries', async () => {
|
||||
await mkdir(path.join(root, '.git', 'rebase-merge'), { recursive: true });
|
||||
await writeFile(path.join(root, '.git', 'MERGE_HEAD'), 'abc123\n', 'utf8');
|
||||
|
||||
await expect(repository.exists(root, '.git/rebase-merge')).resolves.toBe(true);
|
||||
await expect(repository.exists(root, '.git/MERGE_HEAD')).resolves.toBe(true);
|
||||
await expect(repository.exists(root, '.git/rebase-apply')).resolves.toBe(false);
|
||||
await expect(repository.exists(root, '.git/MERGE_HEAD/child')).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('reads and rewrites conflict content without changing file mode', async () => {
|
||||
const conflictPath = path.join(root, 'script.sh');
|
||||
await writeFile(conflictPath, '<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> branch\n', 'utf8');
|
||||
await chmod(conflictPath, 0o755);
|
||||
|
||||
await expect(repository.readText(root, 'script.sh')).resolves.toContain('<<<<<<< HEAD');
|
||||
await repository.writeText(root, 'script.sh', '#!/bin/sh\necho resolved\n');
|
||||
|
||||
await expect(readFile(conflictPath, 'utf8')).resolves.toBe('#!/bin/sh\necho resolved\n');
|
||||
expect((await stat(conflictPath)).mode & 0o777).toBe(0o755);
|
||||
});
|
||||
|
||||
it('allows internal symlinks and rejects traversal or external symlinks', async () => {
|
||||
await writeFile(path.join(root, 'target.txt'), 'inside\n', 'utf8');
|
||||
await symlink(path.join(root, 'target.txt'), path.join(root, 'inside-link.txt'));
|
||||
await expect(repository.readText(root, 'inside-link.txt')).resolves.toBe('inside\n');
|
||||
|
||||
await writeFile(path.join(outsideRoot, 'outside.txt'), 'outside\n', 'utf8');
|
||||
await symlink(path.join(outsideRoot, 'outside.txt'), path.join(root, 'outside-link.txt'));
|
||||
await expect(repository.readText(root, 'outside-link.txt')).rejects.toThrow(
|
||||
/outside the base directory/i
|
||||
);
|
||||
await expect(repository.exists(root, '../outside.txt')).rejects.toThrow(
|
||||
/outside the base directory/i
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects missing, non-file, and oversized conflict content', async () => {
|
||||
await expect(repository.readText(root, 'missing.txt')).rejects.toMatchObject({
|
||||
code: 'ENOENT',
|
||||
});
|
||||
|
||||
await mkdir(path.join(root, 'directory'));
|
||||
await expect(repository.readText(root, 'directory')).rejects.toThrow(/bounded regular file/i);
|
||||
await expect(repository.writeText(root, 'directory', 'content')).rejects.toThrow();
|
||||
|
||||
const oversizedPath = path.join(root, 'oversized.txt');
|
||||
await writeFile(oversizedPath, '', 'utf8');
|
||||
await truncate(oversizedPath, 16 * 1024 * 1024 + 1);
|
||||
await expect(repository.readText(root, 'oversized.txt')).rejects.toThrow(
|
||||
/bounded regular file/i
|
||||
);
|
||||
await expect(
|
||||
repository.writeText(root, 'oversized.txt', 'x'.repeat(16 * 1024 * 1024 + 1))
|
||||
).rejects.toThrow(/16 MiB/i);
|
||||
});
|
||||
});
|
||||
125
server/src/__tests__/delegation-repository.test.ts
Normal file
125
server/src/__tests__/delegation-repository.test.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { DelegationApproval, DelegationSettings } from '@veritas-kanban/shared';
|
||||
import { FileDelegationRepository } from '../storage/delegation-repository.js';
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs/promises')>();
|
||||
return { ...actual, lstat: vi.fn(actual.lstat) };
|
||||
});
|
||||
|
||||
function settings(delegateAgent: string): DelegationSettings {
|
||||
return {
|
||||
enabled: true,
|
||||
delegateAgent,
|
||||
expires: '2026-08-24T00:00:00.000Z',
|
||||
scope: { type: 'all' },
|
||||
createdAt: '2026-08-23T20:00:00.000Z',
|
||||
createdBy: 'brad',
|
||||
};
|
||||
}
|
||||
|
||||
function approval(id: string): DelegationApproval {
|
||||
return {
|
||||
id,
|
||||
taskId: `task-${id}`,
|
||||
taskTitle: `Task ${id}`,
|
||||
agent: 'TARS',
|
||||
delegated: true,
|
||||
timestamp: '2026-08-23T20:00:00.000Z',
|
||||
originalDelegation: 'TARS_2026-08-23T20:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
describe('FileDelegationRepository', () => {
|
||||
let root: string;
|
||||
let runtimeDir: string;
|
||||
let repository: FileDelegationRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(path.join(process.cwd(), '.veritas-delegation-repository-'));
|
||||
runtimeDir = path.join(root, 'runtime');
|
||||
repository = new FileDelegationRepository(runtimeDir);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('reads defaults and persists settings and approvals', async () => {
|
||||
await expect(repository.readSettings()).resolves.toBeNull();
|
||||
await expect(repository.readLog()).resolves.toEqual({ approvals: [] });
|
||||
|
||||
await repository.writeSettings(settings('TARS'));
|
||||
await repository.updateLog(() => ({ approvals: [approval('one')] }));
|
||||
|
||||
await expect(repository.readSettings()).resolves.toEqual(settings('TARS'));
|
||||
await expect(repository.readLog()).resolves.toEqual({ approvals: [approval('one')] });
|
||||
await expect(readFile(path.join(runtimeDir, 'delegation.json'), 'utf8')).resolves.toContain(
|
||||
'"delegateAgent": "TARS"'
|
||||
);
|
||||
});
|
||||
|
||||
it('serializes concurrent settings and log updates', async () => {
|
||||
await repository.writeSettings(settings('TARS'));
|
||||
await Promise.all([
|
||||
repository.updateSettings((current) =>
|
||||
current ? { ...current, excludeTags: [...(current.excludeTags ?? []), 'private'] } : null
|
||||
),
|
||||
repository.updateSettings((current) =>
|
||||
current ? { ...current, excludeTags: [...(current.excludeTags ?? []), 'blocked'] } : null
|
||||
),
|
||||
]);
|
||||
await Promise.all([
|
||||
repository.updateLog((current) => ({ approvals: [...current.approvals, approval('one')] })),
|
||||
repository.updateLog((current) => ({ approvals: [...current.approvals, approval('two')] })),
|
||||
]);
|
||||
|
||||
expect((await repository.readSettings())?.excludeTags).toEqual(
|
||||
expect.arrayContaining(['private', 'blocked'])
|
||||
);
|
||||
expect((await repository.readLog()).approvals.map(({ id }) => id)).toEqual(
|
||||
expect.arrayContaining(['one', 'two'])
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects symbolic links and non-file state paths', async () => {
|
||||
await mkdir(runtimeDir, { recursive: true });
|
||||
const target = path.join(root, 'outside.json');
|
||||
await writeFile(target, JSON.stringify(settings('CASE')), 'utf8');
|
||||
await symlink(target, path.join(runtimeDir, 'delegation.json'));
|
||||
await expect(repository.readSettings()).rejects.toThrow(/symbolic link/i);
|
||||
|
||||
await mkdir(path.join(runtimeDir, 'delegation-log.json'));
|
||||
await expect(repository.readLog()).rejects.toThrow(/bounded regular file/i);
|
||||
});
|
||||
|
||||
it('rejects state replaced after its file handle is opened', async () => {
|
||||
await repository.writeSettings(settings('TARS'));
|
||||
const actual = await vi.importActual<typeof import('node:fs/promises')>('node:fs/promises');
|
||||
vi.mocked(lstat).mockImplementationOnce(async (filePath) => {
|
||||
const stats = await actual.lstat(filePath);
|
||||
return Object.assign(Object.create(Object.getPrototypeOf(stats)), stats, {
|
||||
ino: stats.ino + 1,
|
||||
});
|
||||
});
|
||||
|
||||
await expect(repository.readSettings()).rejects.toThrow(/changed file/i);
|
||||
});
|
||||
|
||||
it('rejects symbolic-link directories and oversized state', async () => {
|
||||
const realDirectory = path.join(root, 'real-runtime');
|
||||
const linkedDirectory = path.join(root, 'linked-runtime');
|
||||
await mkdir(realDirectory);
|
||||
await symlink(realDirectory, linkedDirectory, 'dir');
|
||||
const linkedRepository = new FileDelegationRepository(linkedDirectory);
|
||||
await expect(linkedRepository.writeSettings(settings('TARS'))).rejects.toThrow(
|
||||
/regular directory/i
|
||||
);
|
||||
|
||||
await expect(
|
||||
repository.writeSettings({ ...settings('TARS'), excludeTags: ['x'.repeat(4 * 1024 * 1024)] })
|
||||
).rejects.toThrow(/4 MiB/i);
|
||||
});
|
||||
});
|
||||
|
|
@ -13,6 +13,7 @@ describe('DelegationService', () => {
|
|||
let repoDir: string;
|
||||
let workDir: string;
|
||||
let oldCwd: string;
|
||||
let oldDataDir: string | undefined;
|
||||
let service: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
@ -21,6 +22,8 @@ describe('DelegationService', () => {
|
|||
workDir = path.join(repoDir, 'server');
|
||||
await fs.mkdir(workDir, { recursive: true });
|
||||
oldCwd = process.cwd();
|
||||
oldDataDir = process.env.DATA_DIR;
|
||||
process.env.DATA_DIR = repoDir;
|
||||
process.chdir(workDir);
|
||||
const mod = await import('../services/delegation-service.js');
|
||||
service = new mod.DelegationService();
|
||||
|
|
@ -28,6 +31,8 @@ describe('DelegationService', () => {
|
|||
|
||||
afterEach(async () => {
|
||||
process.chdir(oldCwd);
|
||||
if (oldDataDir === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = oldDataDir;
|
||||
await fs.rm(repoDir, { recursive: true, force: true });
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,15 +5,19 @@ const createFsPromisesMock = vi.hoisted(() => () => {
|
|||
access: vi.fn().mockResolvedValue(undefined),
|
||||
appendFile: vi.fn().mockResolvedValue(undefined),
|
||||
copyFile: vi.fn().mockResolvedValue(undefined),
|
||||
cp: vi.fn().mockResolvedValue(undefined),
|
||||
lstat: vi.fn().mockResolvedValue({ isSymbolicLink: () => false }),
|
||||
mkdir: vi.fn().mockResolvedValue(undefined),
|
||||
open: vi.fn().mockResolvedValue(undefined),
|
||||
readFile: vi.fn().mockResolvedValue(''),
|
||||
writeFile: vi.fn().mockResolvedValue(undefined),
|
||||
readdir: vi.fn().mockResolvedValue([]),
|
||||
rename: vi.fn().mockResolvedValue(undefined),
|
||||
unlink: vi.fn().mockResolvedValue(undefined),
|
||||
rm: vi.fn().mockResolvedValue(undefined),
|
||||
rmdir: vi.fn().mockResolvedValue(undefined),
|
||||
stat: vi.fn().mockResolvedValue({ isDirectory: () => true, size: 0 }),
|
||||
statfs: vi.fn().mockResolvedValue({ bfree: 1, bsize: 1 }),
|
||||
};
|
||||
return { ...mod, default: mod };
|
||||
});
|
||||
|
|
@ -73,6 +77,10 @@ describe('paths: Docker DATA_DIR support', () => {
|
|||
expect(paths.getTasksActiveDir()).toBe('/app/data/tasks/active');
|
||||
expect(paths.getTasksArchiveDir()).toBe('/app/data/tasks/archive');
|
||||
expect(paths.getRuntimeDir()).toBe('/app/data/.veritas-kanban');
|
||||
expect(paths.getLegacyRuntimeDirs()).toEqual(
|
||||
expect.arrayContaining(['/app/data', '/app/.veritas-kanban', '/app/server/.veritas-kanban'])
|
||||
);
|
||||
expect(paths.getLegacyRuntimeDirs()).not.toContain('/app/data/.veritas-kanban');
|
||||
});
|
||||
|
||||
it('TaskService defaults to DATA_DIR-backed task directories when set', async () => {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue