@gobing-ai/spur 0.3.4 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.4",
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",
@@ -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
- # These tests mock the FIRST-PARTY oRPC `api` client (`../../../src/lib/rpc-client`).
39
- # That client is a callable Proxy, so spyOn cannot intercept it (the call regenerates
40
- # and hits the network) — `mock.module()` is the only working mechanism. They are safe
41
- # because they sort AFTER both rpc-client unit-test files (`lib/rpc-client.test.ts`,
42
- # `rpc-client.test.ts`), so the override never leaks back into a real-import reader.
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['\"]"
package/spur.js CHANGED
@@ -70181,7 +70181,7 @@ import { join as join13, resolve as resolve5 } from "path";
70181
70181
  var CLI_CONFIG = {
70182
70182
  binaryName: "spur",
70183
70183
  binaryLabel: "spur",
70184
- binaryVersion: "0.3.4",
70184
+ binaryVersion: "0.3.5",
70185
70185
  configDir: ".spur",
70186
70186
  configFile: ".spur/config.yaml",
70187
70187
  databaseFile: ".spur/spur.db"