substrate-ai 0.20.49 → 0.20.50

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": "substrate-ai",
3
- "version": "0.20.49",
3
+ "version": "0.20.50",
4
4
  "description": "Substrate — multi-agent orchestration daemon for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,312 @@
1
+ ##
2
+ # Probe-Author State-Integrating Defect Corpus
3
+ #
4
+ # Version: v1 (2026-05-03)
5
+ # Substrate version when corpus authored: v0.20.49
6
+ #
7
+ # Purpose: Oracle for the state-integrating eval harness
8
+ # (Story 65-3, scripts/eval-probe-author-state-integrating.mjs).
9
+ # Each entry documents a defect class where the broken implementation
10
+ # reads/writes/queries external state incorrectly. The eval dispatches
11
+ # probe-author against each entry's source_ac and asserts that the
12
+ # authored probe's shape matches the entry's signature regexes.
13
+ #
14
+ # Schema (each applicable_entries item):
15
+ # id — unique kebab-label
16
+ # story_key — optional; reference only
17
+ # description — one-line human label
18
+ # source_ac — AC text sent to probe-author
19
+ # broken_implementation — what the broken impl does
20
+ # real_state_condition — the real-state condition that breaks it
21
+ # signature — non-empty list of regex strings; ALL must
22
+ # match JSON.stringify(probe) for ANY probe
23
+ # mock_authored_probes — ≥1 probe; used in --dry-run mode
24
+ ##
25
+
26
+ applicable_entries:
27
+ - id: entry-1-obs017-git-log-wrong-cwd
28
+ story_key: '2-4'
29
+ description: git log called with cwd=fleetRoot instead of per-project directory
30
+ source_ac: |
31
+ For each project repository in the fleet, the morning briefing generator
32
+ reads the most recent 10 commits authored in the last 24 hours using
33
+ `git log --since=24h --format="%H %s" --no-merges`. Attribution must be
34
+ per-project: commits from repository A must not appear in the briefing
35
+ for repository B. The implementation invokes `git log` with the correct
36
+ `cwd` set to each project's root directory.
37
+ broken_implementation: |
38
+ fetchGitLog() calls `execSync('git log ...', { cwd: fleetRoot })` where
39
+ fleetRoot is the top-level directory containing all project subdirectories.
40
+ All commits from all repos are attributed to every project, causing
41
+ cross-project commit leakage.
42
+ real_state_condition: |
43
+ Two or more git repositories exist as sibling directories under a common
44
+ parent. Each repo has distinct commits. The broken implementation returns
45
+ all commits regardless of which project directory is requested.
46
+ signature:
47
+ - 'git\s+log'
48
+ - 'alpha|beta|fleet|project.?[AB]|repo.?[12]|tmpdir|mkdtemp'
49
+ mock_authored_probes:
50
+ - name: per-project-git-log-attribution
51
+ sandbox: host
52
+ command: |
53
+ # Create two sibling repos with distinct commits
54
+ PARENT=$(mktemp -d)
55
+ mkdir -p "$PARENT/repo-alpha" "$PARENT/repo-beta"
56
+ cd "$PARENT/repo-alpha" && git init && git config user.email "t@t" && git config user.name "T"
57
+ echo "alpha" > a.txt && git add . && git commit -m "alpha commit"
58
+ cd "$PARENT/repo-beta" && git init && git config user.email "t@t" && git config user.name "T"
59
+ echo "beta" > b.txt && git add . && git commit -m "beta commit"
60
+ # Probe: verify git log is called per-project, not for the fleet root
61
+ # Run git log for repo-alpha and confirm only alpha commits appear
62
+ OUTPUT=$(cd "$PARENT/repo-alpha" && git log --format="%s" 2>&1)
63
+ echo "$OUTPUT"
64
+ echo "$OUTPUT" | grep -q "alpha commit" && echo "alpha-found" || echo "alpha-missing"
65
+ echo "$OUTPUT" | grep -q "beta commit" && echo "beta-found" || echo "beta-missing"
66
+ expect_stdout_regex:
67
+ - 'alpha-found'
68
+ expect_stdout_no_regex:
69
+ - 'beta-found'
70
+
71
+ - id: entry-2-subprocess-synthesized-vs-real
72
+ story_key: '2-5'
73
+ description: npm outdated called with mocked input instead of real subprocess
74
+ source_ac: |
75
+ The dependency staleness checker invokes `npm outdated --json` as a real
76
+ subprocess in the project directory. The output JSON is parsed to identify
77
+ packages where the `current` version is behind `wanted` or `latest`. The
78
+ implementation must NOT use cached or synthetic data; it must invoke npm
79
+ as an external process each time.
80
+ broken_implementation: |
81
+ runDependencyCheck() returns a hardcoded JSON fixture `{ "lodash": { "current":
82
+ "4.17.20", "wanted": "4.17.21", "latest": "4.17.21" } }` instead of
83
+ spawning a real npm process. Tests pass but the feature silently shows
84
+ stale data in production.
85
+ real_state_condition: |
86
+ A real npm project directory with a package.json exists on disk. The
87
+ real npm outdated output differs from the hardcoded fixture (either
88
+ empty or different packages).
89
+ signature:
90
+ - 'npm\s+outdated'
91
+ - 'current|wanted|latest|outdated'
92
+ mock_authored_probes:
93
+ - name: npm-outdated-real-subprocess
94
+ sandbox: host
95
+ command: |
96
+ # Create a minimal npm project
97
+ TMPDIR=$(mktemp -d)
98
+ cd "$TMPDIR"
99
+ echo '{"name":"probe-test","version":"1.0.0","dependencies":{}}' > package.json
100
+ npm install --silent
101
+ # Run the implementation — it must invoke npm outdated, not return a fixture
102
+ node dist/cli/index.js dependency-check --project "$TMPDIR" --output json
103
+ expect_stdout_regex:
104
+ - '\{|\[|dependencies|outdated|packages'
105
+ expect_stdout_no_regex:
106
+ - 'fixture|mock|synthetic|hardcoded'
107
+
108
+ - id: entry-3-tilde-path-not-expanded
109
+ story_key: '2-6'
110
+ description: fs.readFileSync called with literal tilde path instead of expanded home directory
111
+ source_ac: |
112
+ The configuration reader reads the user's config file at `~/.config/myapp/config.json`.
113
+ The path must be expanded to the actual home directory before reading. The implementation
114
+ uses `os.homedir()` or equivalent to resolve `~` to the absolute path. A literal
115
+ `~` in the path passed to `fs.readFileSync` is not a valid filesystem path on Linux/macOS.
116
+ broken_implementation: |
117
+ configReader() calls `fs.readFileSync('~/.config/myapp/config.json', 'utf8')`.
118
+ The literal tilde is not expanded by Node.js's fs module, causing ENOENT
119
+ on every read even when the config file exists at the real path.
120
+ real_state_condition: |
121
+ The config file exists at the real expanded path (e.g., /home/user/.config/myapp/config.json)
122
+ but does NOT exist at the literal path `~/.config/myapp/config.json`. The broken
123
+ implementation throws ENOENT while the correct implementation reads successfully.
124
+ signature:
125
+ - 'HOME|homedir\(\)|\$HOME|home.*dir'
126
+ - '\.config|config\.json|tilde|~'
127
+ mock_authored_probes:
128
+ - name: config-path-tilde-expansion
129
+ sandbox: host
130
+ command: |
131
+ # Ensure config file exists at real expanded path
132
+ REAL_CONFIG="$HOME/.config/myapp/config.json"
133
+ mkdir -p "$(dirname "$REAL_CONFIG")"
134
+ echo '{"setting":"value"}' > "$REAL_CONFIG"
135
+ # Implementation must read it successfully
136
+ node dist/cli/index.js config read
137
+ expect_stdout_regex:
138
+ - 'setting|value|config'
139
+ expect_stdout_no_regex:
140
+ - 'ENOENT|no such file|tilde'
141
+
142
+ - id: entry-4-db-mocked-vs-real
143
+ story_key: '2-7'
144
+ description: DB query returns canned response instead of real Dolt wg_stories state
145
+ source_ac: |
146
+ The sprint planning report queries the `wg_stories` Dolt table for all
147
+ stories with `status = 'PLANNED'` in the current sprint. The query must
148
+ execute against the live Dolt database; the result count must reflect
149
+ the actual number of planned stories. Using an in-memory mock or
150
+ hardcoded fixture is not acceptable.
151
+ broken_implementation: |
152
+ queryPlannedStories() returns a hardcoded array `[{ id: 'fake-1', status:
153
+ 'PLANNED', sprint: 1 }]` without connecting to Dolt. The sprint planning
154
+ report always shows 1 planned story regardless of actual database state.
155
+ real_state_condition: |
156
+ The Dolt database has 0 or ≥2 planned stories. The broken implementation
157
+ always returns exactly 1, masking the real state.
158
+ signature:
159
+ - 'dolt|mysql|wg_stories|planned.stories'
160
+ - 'PLANNED|status.*planned|count|rowCount'
161
+ mock_authored_probes:
162
+ - name: wg-stories-planned-count-from-db
163
+ sandbox: host
164
+ command: |
165
+ # Query the real Dolt database for planned stories
166
+ node dist/cli/index.js status --output-format json | \
167
+ node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); \
168
+ console.log(JSON.stringify({planned: (d.stories||[]).filter(s=>s.status==='PLANNED').length}))"
169
+ expect_stdout_regex:
170
+ - 'planned'
171
+ expect_stdout_no_regex:
172
+ - 'mock|fixture|fake|hardcoded'
173
+
174
+ - id: entry-5-network-mocked-vs-real
175
+ story_key: '2-8'
176
+ description: npm registry fetch intercepted by test double instead of real network call
177
+ source_ac: |
178
+ The version checker fetches the latest published version of a package from
179
+ the npm registry using `npm view <package> version` or `https://registry.npmjs.org/<package>/latest`.
180
+ The fetch must reach the real npm registry; intercepted or cached responses
181
+ are not acceptable. The result is compared against the locally installed version.
182
+ broken_implementation: |
183
+ getLatestVersion() returns a hardcoded version string `"1.2.3"` without
184
+ making any network request. The comparison always uses stale data, masking
185
+ cases where the registry has a newer version.
186
+ real_state_condition: |
187
+ The real npm registry has a version for the package that may differ from
188
+ the hardcoded `"1.2.3"`. The broken implementation always returns `"1.2.3"`.
189
+ signature:
190
+ - 'npm\s+view|registry\.npmjs|npm.*version'
191
+ - 'latest|version|registry|current'
192
+ mock_authored_probes:
193
+ - name: npm-registry-fetch-real-version
194
+ sandbox: host
195
+ command: |
196
+ # Verify the version checker reaches the real registry
197
+ node dist/cli/index.js check-version --package js-yaml --output json
198
+ expect_stdout_regex:
199
+ - 'version|latest|current'
200
+ expect_stdout_no_regex:
201
+ - '1\.2\.3|hardcoded|mock|fixture'
202
+
203
+ - id: entry-6-registry-scan-single-vs-multi
204
+ story_key: '2-9'
205
+ description: registry scan passes on single-package workspace but fails on multi-package monorepo
206
+ source_ac: |
207
+ The workspace version-constraint scanner reads all `package.json` files in
208
+ a monorepo workspace and reports packages where the declared version in one
209
+ workspace package conflicts with the version declared in another. The scanner
210
+ must handle workspaces with ≥2 packages. A single-package workspace is not
211
+ a valid test fixture for cross-package constraint detection.
212
+ broken_implementation: |
213
+ scanVersionConstraints() only reads the root `package.json` and ignores
214
+ sub-package `package.json` files. It passes on single-package workspaces
215
+ but misses cross-package conflicts in real multi-package monorepos.
216
+ real_state_condition: |
217
+ A workspace with ≥2 packages exists, where package A declares `"lodash": "^4.17.0"`
218
+ and package B declares `"lodash": "^3.10.0"`. The broken implementation
219
+ reports no conflicts; the correct implementation reports the constraint mismatch.
220
+ signature:
221
+ - 'mktemp|tmpdir|tmp.*dir'
222
+ - 'package\.json|workspace|monorepo|packages'
223
+ mock_authored_probes:
224
+ - name: multi-package-version-constraint-scan
225
+ sandbox: host
226
+ command: |
227
+ # Create a two-package monorepo fixture
228
+ ROOT=$(mktemp -d)
229
+ mkdir -p "$ROOT/packages/pkg-a" "$ROOT/packages/pkg-b"
230
+ echo '{"name":"root","workspaces":["packages/*"]}' > "$ROOT/package.json"
231
+ echo '{"name":"pkg-a","dependencies":{"lodash":"^4.17.0"}}' > "$ROOT/packages/pkg-a/package.json"
232
+ echo '{"name":"pkg-b","dependencies":{"lodash":"^3.10.0"}}' > "$ROOT/packages/pkg-b/package.json"
233
+ node dist/cli/index.js scan-constraints --root "$ROOT" --output json
234
+ expect_stdout_regex:
235
+ - 'conflict|mismatch|lodash|constraint'
236
+ expect_stdout_no_regex:
237
+ - 'no conflicts|clean|ok'
238
+
239
+ - id: entry-7-git-op-empty-vs-real-repo
240
+ story_key: '2-10'
241
+ description: git tag/describe returns empty on empty repo but silently passes
242
+ source_ac: |
243
+ The release versioner reads the latest git tag from the repository using
244
+ `git tag --sort=-version:refname` or `git describe --tags --abbrev=0`.
245
+ The result must be a non-empty string representing the most recent version tag.
246
+ On a repository with no tags, the implementation must return an explicit error
247
+ or default value, not an empty string silently treated as a valid version.
248
+ broken_implementation: |
249
+ getLatestTag() runs `git tag` in the repo and returns the first line of output.
250
+ On a fresh repository with no tags, `git tag` returns empty output, and the
251
+ function returns an empty string `""` which is used as a version string
252
+ without validation, causing downstream failures.
253
+ real_state_condition: |
254
+ A git repository with ≥1 annotated or lightweight tag exists. The probe
255
+ must assert that the output is a non-empty version string matching a semver
256
+ pattern (e.g., `v1.0.0` or `1.0.0`).
257
+ signature:
258
+ - 'git\s+tag|git\s+describe'
259
+ - 'v\d+\.\d+|semver|non.?empty|tag.*version|version.*tag'
260
+ mock_authored_probes:
261
+ - name: git-tag-non-empty-assertion
262
+ sandbox: host
263
+ command: |
264
+ # Create a git repo with a real tag
265
+ REPO=$(mktemp -d)
266
+ cd "$REPO" && git init && git config user.email "t@t" && git config user.name "T"
267
+ echo "init" > README.md && git add . && git commit -m "initial"
268
+ git tag v1.0.0
269
+ # Run the versioner — must return the tag, not empty
270
+ node dist/cli/index.js get-version --repo "$REPO"
271
+ expect_stdout_regex:
272
+ - 'v1\.0\.0|1\.0\.0'
273
+ expect_stdout_no_regex:
274
+ - '^$|empty|undefined|null'
275
+
276
+ - id: entry-8-spawn-swallows-nonzero-exit
277
+ story_key: '2-11'
278
+ description: spawn invocation ignores non-zero exit code from tsc, masking TypeScript errors
279
+ source_ac: |
280
+ The TypeScript validation step runs `tsc --noEmit` to check for compile
281
+ errors. If `tsc` exits with a non-zero exit code, the validation must
282
+ report failure. The implementation must not swallow the exit code; a
283
+ TypeScript compile error in the project must cause the validation to
284
+ return a failure result, not a success result.
285
+ broken_implementation: |
286
+ runTscCheck() spawns `tsc --noEmit` but wraps the call in a try/catch
287
+ that catches ENOENT and sets result.success=true on any other error
288
+ (including non-zero exit). TypeScript compile errors are silently
289
+ treated as validation success.
290
+ real_state_condition: |
291
+ A TypeScript file with a deliberate type error (e.g., `const x: number = "string"`)
292
+ exists. The broken implementation reports success; the correct implementation
293
+ reports failure with the tsc error output.
294
+ signature:
295
+ - 'tsc'
296
+ - 'exit.*code|exitCode|nonzero|non.zero|status.*[^0]|process\.exit'
297
+ mock_authored_probes:
298
+ - name: tsc-nonzero-exit-detected
299
+ sandbox: host
300
+ command: |
301
+ # Create a TS file with a deliberate type error
302
+ TMPDIR=$(mktemp -d)
303
+ echo '{"compilerOptions":{"strict":true,"noEmit":true}}' > "$TMPDIR/tsconfig.json"
304
+ echo 'const x: number = "this is a string error";' > "$TMPDIR/bad.ts"
305
+ # Run the TypeScript validation — must detect and report failure
306
+ node dist/cli/index.js validate-ts --project "$TMPDIR" --output json; true
307
+ expect_stdout_regex:
308
+ - 'error|fail|invalid|type.*error|tsc.*error'
309
+ expect_stdout_no_regex:
310
+ - '"success":true|"valid":true|"passed":true'
311
+
312
+ excluded_entries: []