@gobing-ai/spur 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -8
- package/spur-cli/config/rules/quality/tsdoc-exports.yaml +1 -1
- package/spur-cli/config/rules/typescript/guarded-happy-dom-register.yaml +64 -0
- package/spur-cli/config/rules/typescript/no-leaky-module-mocks.yaml +8 -9
- package/spur-cli/config/rules/typescript/no-module-scope-import-calls.yaml +61 -0
- package/spur-cli/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +60 -0
- package/spur-cli/config/workflows/task-pipeline.yaml +1 -1
- package/spur.js +807 -350
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spur",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@commander-js/extra-typings": "^14.0.0",
|
|
52
|
-
"@gobing-ai/ts-ai-runner": "^0.4.
|
|
53
|
-
"@gobing-ai/ts-dual-workflow-engine": "^0.4.
|
|
54
|
-
"@gobing-ai/ts-infra": "^0.4.
|
|
55
|
-
"@gobing-ai/ts-llm-jsonl-importer": "^0.4.
|
|
56
|
-
"@gobing-ai/ts-rule-engine": "^0.4.
|
|
57
|
-
"@gobing-ai/ts-runtime": "^0.4.
|
|
58
|
-
"@gobing-ai/ts-utils": "^0.4.
|
|
52
|
+
"@gobing-ai/ts-ai-runner": "^0.4.4",
|
|
53
|
+
"@gobing-ai/ts-dual-workflow-engine": "^0.4.4",
|
|
54
|
+
"@gobing-ai/ts-infra": "^0.4.4",
|
|
55
|
+
"@gobing-ai/ts-llm-jsonl-importer": "^0.4.4",
|
|
56
|
+
"@gobing-ai/ts-rule-engine": "^0.4.4",
|
|
57
|
+
"@gobing-ai/ts-runtime": "^0.4.4",
|
|
58
|
+
"@gobing-ai/ts-utils": "^0.4.4",
|
|
59
59
|
"@types/bun": "1.3.14",
|
|
60
60
|
"@types/figlet": "^1.7.0",
|
|
61
61
|
"@types/node-notifier": "8.0.5",
|
|
@@ -23,7 +23,7 @@ exclude:
|
|
|
23
23
|
rules:
|
|
24
24
|
- id: every-export-has-tsdoc
|
|
25
25
|
description: >
|
|
26
|
-
Exported declarations must have a preceding
|
|
26
|
+
Exported declarations must have a preceding doc comment (JSDoc `/** */` or line `//`).
|
|
27
27
|
severity: error
|
|
28
28
|
enabled: true
|
|
29
29
|
evaluator:
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
$schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
|
|
2
|
+
# Require try/catch guard around GlobalRegistrator.register() in web tests.
|
|
3
|
+
#
|
|
4
|
+
# WHY: GlobalRegistrator.register() sets up happy-dom globals (window, document,
|
|
5
|
+
# etc.). In a full bun test suite, multiple test files call it. The first call
|
|
6
|
+
# succeeds; subsequent calls throw "Happy DOM has already been globally
|
|
7
|
+
# registered" and the error kills the test file (4 test files × ~13 tests each
|
|
8
|
+
# = 52 tests lost). This only surfaces when ALL tests run together (CI, or
|
|
9
|
+
# `bun test ./apps/web/`), not when a single file runs in isolation.
|
|
10
|
+
# Real incident: 52 CI-only failures across 4 test files, 2026-07-09.
|
|
11
|
+
#
|
|
12
|
+
# FIX: wrap in try/catch:
|
|
13
|
+
#
|
|
14
|
+
# try { GlobalRegistrator.register(); } catch {} // already registered in suite
|
|
15
|
+
#
|
|
16
|
+
# The teardown helper (apps/web/tests/happy-dom.ts) is the sanctioned unregister
|
|
17
|
+
# caller (see happy-dom-teardown.yaml). This rule guards the register side.
|
|
18
|
+
#
|
|
19
|
+
# Severity warning: the file may pass in isolation but will fail in the full
|
|
20
|
+
# suite; fix before merge.
|
|
21
|
+
include:
|
|
22
|
+
- "apps/web/tests/**/*.test.ts"
|
|
23
|
+
- "apps/web/tests/**/*.test.tsx"
|
|
24
|
+
exclude:
|
|
25
|
+
- "**/node_modules/**"
|
|
26
|
+
- "**/dist/**"
|
|
27
|
+
# Helper that manages happy-dom lifecycle — not a raw register call.
|
|
28
|
+
- "apps/web/tests/happy-dom.ts"
|
|
29
|
+
# These files already wrap register() in try/catch; they are compliant.
|
|
30
|
+
- "apps/web/tests/modules/task-kanban/board.test.tsx"
|
|
31
|
+
- "apps/web/tests/modules/task-kanban/components.test.tsx"
|
|
32
|
+
- "apps/web/tests/modules/task-kanban/task-detail.test.tsx"
|
|
33
|
+
- "apps/web/tests/modules/task-kanban/index.test.tsx"
|
|
34
|
+
- "apps/web/tests/modules/task-kanban/new-task-panel.test.tsx"
|
|
35
|
+
- "apps/web/tests/modules/task-kanban/markdown-body.test.tsx"
|
|
36
|
+
- "apps/web/tests/modules/task-kanban/useTaskParams.test.tsx"
|
|
37
|
+
- "apps/web/tests/modules/task-kanban/task-filters.test.tsx"
|
|
38
|
+
- "apps/web/tests/modules/task-kanban/useTasks.test.ts"
|
|
39
|
+
- "apps/web/tests/modules/features/components.test.tsx"
|
|
40
|
+
- "apps/web/tests/lib/theme.test.ts"
|
|
41
|
+
- "apps/web/tests/components/BoardLayout.test.tsx"
|
|
42
|
+
- "apps/web/tests/modules/observability/components.test.tsx"
|
|
43
|
+
- "apps/web/tests/components/ThemeToggle.test.tsx"
|
|
44
|
+
- "apps/web/tests/components/ResponsiveAndTheme.test.tsx"
|
|
45
|
+
|
|
46
|
+
rules:
|
|
47
|
+
- id: guarded-happy-dom-register
|
|
48
|
+
description: >
|
|
49
|
+
GlobalRegistrator.register() must be wrapped in try/catch — without
|
|
50
|
+
it, the second test file that calls it throws "already been globally
|
|
51
|
+
registered" and all tests in that file are lost (a CI-only failure
|
|
52
|
+
that never reproduces when a single file runs in isolation). Wrap:
|
|
53
|
+
`try { GlobalRegistrator.register(); } catch {}`. 52-real CI incidents,
|
|
54
|
+
2026-07-09.
|
|
55
|
+
severity: warning
|
|
56
|
+
evaluator:
|
|
57
|
+
type: rg
|
|
58
|
+
config:
|
|
59
|
+
# Match bare GlobalRegistrator.register() — NOT inside try/catch
|
|
60
|
+
# Negative lookbehind: line does NOT start with "try" before the call
|
|
61
|
+
pattern: "GlobalRegistrator\\.register\\(\\)"
|
|
62
|
+
# Only flag lines where the call is NOT preceded by try{ on the same line
|
|
63
|
+
# (the fixed version is `try { GlobalRegistrator.register(); } catch {}`)
|
|
64
|
+
# We match the call itself and let the description guide the fix
|
|
@@ -35,21 +35,20 @@ include:
|
|
|
35
35
|
exclude:
|
|
36
36
|
- "**/node_modules/**"
|
|
37
37
|
- "**/dist/**"
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# Keep this list minimal: only files that mock a first-party module and have no
|
|
44
|
-
# spyOn-able alternative belong here. (markdown-body mocks only third-party modules —
|
|
45
|
-
# mermaid/dompurify — which the narrowed pattern no longer flags, so it is not listed.)
|
|
38
|
+
# Shared full-surface mock helpers are the CORRECT pattern — they ensure every
|
|
39
|
+
# test file registers the same compatible surface, neutralizing "last mock wins."
|
|
40
|
+
- "apps/web/tests/test-helpers/rpc-client-mock.ts"
|
|
41
|
+
# These files use beforeEach restoreMock() atop the shared baseline — the mock.module
|
|
42
|
+
# call is in a runtime function, not at module scope, and always registers the full surface.
|
|
46
43
|
- "apps/web/tests/modules/task-kanban/board.test.tsx"
|
|
47
44
|
- "apps/web/tests/modules/task-kanban/components.test.tsx"
|
|
48
45
|
- "apps/web/tests/modules/task-kanban/new-task-panel.test.tsx"
|
|
49
46
|
- "apps/web/tests/modules/features/components.test.tsx"
|
|
50
47
|
- "apps/web/tests/modules/task-kanban/task-detail.test.tsx"
|
|
51
48
|
- "apps/web/tests/modules/task-kanban/index.test.tsx"
|
|
52
|
-
|
|
49
|
+
# index.test.tsx no longer mocks useTasks/useTaskParams/TaskDetail; useTasks.test.ts
|
|
50
|
+
# uses custom listFn (isolated store), not mock.module for first-party modules.
|
|
51
|
+
- "apps/web/tests/modules/task-kanban/useTasks.test.ts"
|
|
53
52
|
rules:
|
|
54
53
|
- id: no-leaky-mock-module
|
|
55
54
|
description: >
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
$schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
|
|
2
|
+
# Forbid calling imported functions at module-evaluation scope in source files.
|
|
3
|
+
#
|
|
4
|
+
# WHY: calling an imported function at module scope (outside any function/class
|
|
5
|
+
# body) creates an evaluation-time side effect that captures the depencency
|
|
6
|
+
# before any test mock can intercept it. If the first test file that evaluates
|
|
7
|
+
# this module has a mock active, the module caches with that mock's state; in
|
|
8
|
+
# CI (where test-file order differs from local), a different test may evaluate
|
|
9
|
+
# it first with NO mock or a different mock, causing CI-only failures that pass
|
|
10
|
+
# locally every time. Real incidents:
|
|
11
|
+
#
|
|
12
|
+
# 2026-07-09: useTasks.ts:6 — const SSE_URL = `${resolveApiUrl()}/...` (73 failures)
|
|
13
|
+
# 2026-07-09: SystemEventsTab.tsx:42-43 — const HISTORY_URL = `${resolveApiUrl()}/...`;
|
|
14
|
+
# const SSE_URL = `${resolveApiUrl()}/...` (1 failure)
|
|
15
|
+
#
|
|
16
|
+
# The pattern is always the same: a top-level `const` or `let` whose initializer
|
|
17
|
+
# calls a function imported from another module. Defer the call to a function:
|
|
18
|
+
#
|
|
19
|
+
# // BEFORE (broken):
|
|
20
|
+
# const SSE_URL = `${resolveApiUrl()}/events/planning`;
|
|
21
|
+
#
|
|
22
|
+
# // AFTER (fixed):
|
|
23
|
+
# const sseUrl = () => `${resolveApiUrl()}/events/planning`;
|
|
24
|
+
#
|
|
25
|
+
# FIX: wrap the initializer in an arrow function and call it lazily at the
|
|
26
|
+
# point of use. If the value is used as a default parameter or a constant
|
|
27
|
+
# reference, convert it to a getter function or compute it in the
|
|
28
|
+
# constructor/init method.
|
|
29
|
+
#
|
|
30
|
+
# Severity: warning. A hit may pass locally but WILL fail in CI if a mock is
|
|
31
|
+
# involved; justify with a comment or convert to lazy.
|
|
32
|
+
include:
|
|
33
|
+
- "apps/**/src/**/*.ts"
|
|
34
|
+
- "apps/**/src/**/*.tsx"
|
|
35
|
+
- "packages/**/src/**/*.ts"
|
|
36
|
+
- "packages/**/src/**/*.tsx"
|
|
37
|
+
exclude:
|
|
38
|
+
- "**/node_modules/**"
|
|
39
|
+
- "**/dist/**"
|
|
40
|
+
- "**/tests/**"
|
|
41
|
+
- "**/*.test.ts"
|
|
42
|
+
- "**/*.test.tsx"
|
|
43
|
+
|
|
44
|
+
rules:
|
|
45
|
+
- id: no-module-scope-import-calls
|
|
46
|
+
description: >
|
|
47
|
+
Calling an imported function at module scope (outside any function or
|
|
48
|
+
class body) creates an evaluation-time side effect that captures the
|
|
49
|
+
dependency before any test mock can intercept it — a CI-only,
|
|
50
|
+
ordering-dependent failure. Defer the call: wrap the value in an arrow
|
|
51
|
+
function and call it lazily at the point of use. Real incidents:
|
|
52
|
+
useTasks.ts:6 (73 CI failures), SystemEventsTab.tsx:42 (1 CI failure),
|
|
53
|
+
2026-07-09.
|
|
54
|
+
severity: warning
|
|
55
|
+
evaluator:
|
|
56
|
+
type: rg
|
|
57
|
+
config:
|
|
58
|
+
# Match top-level const/let whose initializer is a template literal
|
|
59
|
+
# calling an import (e.g. resolveApiUrl(), api.*, ...).
|
|
60
|
+
# The pattern: line starts with `const`/`let`, contains `${...()}`.
|
|
61
|
+
pattern: "^(const|let)\\s+\\w+\\s*=\\s*`[^`]*\\$\\{[^}]*\\([^)]*\\)[^}]*\\}[^`]*`"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
$schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
|
|
2
|
+
# Require test files to mock dependencies of modules with module-eval side effects.
|
|
3
|
+
#
|
|
4
|
+
# WHY (the general pattern): some source modules run code at module-evaluation
|
|
5
|
+
# time — creating singletons, computing constants from imports, calling functions
|
|
6
|
+
# from other modules at module scope (not inside a function/class body). When a
|
|
7
|
+
# test file imports such a module, it triggers the side effect. If the dependency
|
|
8
|
+
# modules are not mocked, the side effect captures the real API and that state
|
|
9
|
+
# persists across the entire test suite — a CI-only, file-ordering-dependent
|
|
10
|
+
# failure that never reproduces locally. The sibling rule `no-leaky-module-mocks`
|
|
11
|
+
# handles the mirror problem (mock.module leaking into real-import readers).
|
|
12
|
+
#
|
|
13
|
+
# KNOWN MODULES WITH MODULE-EVAL SIDE EFFECTS:
|
|
14
|
+
#
|
|
15
|
+
# (None currently active — the useTasks.ts singleton side effect was fixed by
|
|
16
|
+
# commit 3937d31 which converted resolveApiUrl() and api.task.list() calls to
|
|
17
|
+
# lazy dynamic imports. The singleton _sharedStore = new TaskStore() no longer
|
|
18
|
+
# triggers an API call at module-eval time. When a new side-effect module is
|
|
19
|
+
# discovered, add it here with the pattern shown below.)
|
|
20
|
+
#
|
|
21
|
+
# RESOLVED — src/modules/task-kanban/useTasks.ts (fixed 2026-07-08)
|
|
22
|
+
# - Line 6: was const SSE_URL = `${resolveApiUrl()}/events/planning`;
|
|
23
|
+
# → now: const sseUrl = async () => { const { resolveApiUrl } = await import(...); return ...; };
|
|
24
|
+
# - Line 159: was const sharedStore = new TaskStore(); (calls defaultListTasks
|
|
25
|
+
# which statically imported api.task.list) → now: defaultListTasks uses
|
|
26
|
+
# dynamic await import('../../lib/rpc-client'), so construction is lazy.
|
|
27
|
+
# - 73 CI failures, 2026-07-08. Fix: commit 3937d31.
|
|
28
|
+
#
|
|
29
|
+
# To ADD a new module to this list when another CI-only failure is traced to a
|
|
30
|
+
# module-eval side effect: append its import pattern to `rules[].evaluator.config.pattern`
|
|
31
|
+
# (alternation) and add the corresponding entry above.
|
|
32
|
+
include:
|
|
33
|
+
- "apps/**/tests/**/*.test.ts"
|
|
34
|
+
- "apps/**/tests/**/*.test.tsx"
|
|
35
|
+
- "packages/**/tests/**/*.test.ts"
|
|
36
|
+
- "packages/**/tests/**/*.test.tsx"
|
|
37
|
+
exclude:
|
|
38
|
+
- "**/node_modules/**"
|
|
39
|
+
- "**/dist/**"
|
|
40
|
+
# All known side-effect modules are resolved. When a new one is discovered,
|
|
41
|
+
# exclude files that already mock the dependency before importing it.
|
|
42
|
+
|
|
43
|
+
rules:
|
|
44
|
+
- id: no-unmocked-module-eval-side-effects
|
|
45
|
+
description: >
|
|
46
|
+
Test files that import a module with module-evaluation side effects
|
|
47
|
+
(singletons created at module scope, imports called at eval time, etc.)
|
|
48
|
+
MUST mock the dependency modules BEFORE the import. Without the mock,
|
|
49
|
+
the side effect captures the real API and leaks into every subsequent
|
|
50
|
+
test file — a CI-only, ordering-dependent failure. Add mock.module()
|
|
51
|
+
for the listed dependency before any import of the side-effect module.
|
|
52
|
+
See the file header for the list of known side-effect modules and their
|
|
53
|
+
dependencies. 73-real CI incidents (useTasks → rpc-client), 2026-07-09.
|
|
54
|
+
severity: warning
|
|
55
|
+
evaluator:
|
|
56
|
+
type: rg
|
|
57
|
+
config:
|
|
58
|
+
# Match imports from modules with known module-eval side effects.
|
|
59
|
+
# Add new patterns here as alternations (|) when new cases are discovered.
|
|
60
|
+
pattern: "from ['\"].*task-kanban/useTasks['\"]"
|
|
@@ -120,7 +120,7 @@ states:
|
|
|
120
120
|
command: "bun run lint"
|
|
121
121
|
|
|
122
122
|
- id: review
|
|
123
|
-
description:
|
|
123
|
+
description: Three-dimensional code review via /sp:dev-review (functional requirements traceability + SECUA framework (Security, Efficiency, Correctness, Usability, Architecture) + architecture depth), findings written to `## Review`.
|
|
124
124
|
onEnter:
|
|
125
125
|
- kind: agent.run
|
|
126
126
|
options:
|