@massu/core 2.5.1 → 2.5.2

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/CHANGELOG.md CHANGED
@@ -10,6 +10,176 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
10
10
  > redistributed under the **Apache License 2.0**. Full license + NOTICE:
11
11
  > `packages/core/assets/embedder/MODEL-LICENSE`.
12
12
 
13
+ ## [2.5.2] - 2026-08-14
14
+
15
+ ### Fixed
16
+
17
+ - **The code-intelligence index was empty, and an empty index looked exactly like a project
18
+ with no imports.** Ten places across seven modules each answered "where does this project's
19
+ source live?" with the literal `src/`. In any layout that is not single-package every one of
20
+ them matched nothing: the import index consumed **0 of 1266** indexed files, the page index
21
+ found **0 of 69** real pages, cross-domain analysis saw one package of four, and two hooks
22
+ exited silently on every edit and every deletion. Nothing reported it, because a builder that
23
+ reads no rows returns `0` and the caller prints that as a normal result.
24
+
25
+ The layout now declares itself in `massu.config.yaml` and every consumer asks one module.
26
+ Everything is derived from declarations that already existed — `paths.source`,
27
+ `paths.monorepo_roots`, `framework.languages.*.source_dirs`, `paths.pages`,
28
+ `paths.components` — and one definition feeds both the SQL predicates and the in-JS path
29
+ guards, so widening a query cannot leave its recursion guard behind. A builder whose declared
30
+ source dirs match **0 rows of a non-empty** file table now raises an error naming the
31
+ denominator, the declaration, and the prefixes actually present. The contract is `0-of-N`,
32
+ never `0`: a repo with an empty or not-yet-built index legitimately matches nothing and must
33
+ not trip it.
34
+
35
+ Measured after, on the same corpus: 1128 of 1266 files consumed, 1096 import edges where the
36
+ table had been empty, 69 pages indexed, and every downstream read site non-empty.
37
+
38
+ - **A failed knowledge re-index was indistinguishable from a successful one.** The helper that
39
+ refreshes the markdown corpus caught every exception without binding it and returned a boolean
40
+ the only call site discarded. A corrupt database, a permissions error and a search-index
41
+ failure therefore produced byte-identical output to a healthy rebuild: the tool answered from
42
+ whatever the index held before and said nothing.
43
+
44
+ The error is now bound and recorded, and a failed refresh prepends a notice to the response so
45
+ the caller knows the answer may be stale. Loud is not fatal — the tool still answers from the
46
+ index it has, because turning a degradation into an outage for all twelve knowledge tools is
47
+ what the "degrade gracefully" contract exists to avoid.
48
+
49
+ The rebuild itself is no longer a side effect of dispatching. It cost 677 documents / 11,954
50
+ chunks on any file-modification under four scanned roots, and no caller had asked for it;
51
+ callers now state their intent explicitly, with no default, so an unrequested full rebuild
52
+ cannot happen by omission.
53
+
54
+ - **The session-end fix reminder fired on other sessions' work, and could not be discharged.**
55
+ Three defects compounded. Documentation was excluded from two of the three git reads but not
56
+ from the one whose text is actually scored, so the exclusion held only when a diff was
57
+ documentation-only — change any code file and prose was scored as code again. The diff was
58
+ read over the whole working tree with no notion of who made the changes, so on a machine where
59
+ several sessions hold one repository at once the reminder routinely demanded an artifact for
60
+ work the blocked session had not done. And the `autoLearning.fixDetection.enabled` switch,
61
+ which reads as the control for fix detection, never reached this reminder at all.
62
+
63
+ All three git reads now carry the documentation exclusion; the scan is intersected with files
64
+ the acting session actually touched, including the body read and not merely the gate; and the
65
+ switch gates both halves. When the actor cannot be established the reminder stays **silent** —
66
+ a demand that no legal action can discharge is one people route around, after which nothing is
67
+ enforced. The verdict carries a reason and a scanned-file count, so a negative result is never
68
+ an unexplained silence.
69
+
70
+ - **The reality gate's hook check was red forever, and the only way to green it was to
71
+ destroy its own evidence.** R-3 failed on the LIFETIME row count of
72
+ `.massu/hook-failures.jsonl`, an append-only log retained on purpose. From the first
73
+ failure ever recorded the gate stayed red permanently — at 11,116 rows it had long since
74
+ stopped carrying information, and a gate whose sole remedy is deleting its input is one
75
+ people learn to route around.
76
+
77
+ The predicate is now RECENCY: the check fails when a failure lies inside a bounded window
78
+ (default 24h, `MASSU_HOOK_FAILURE_WINDOW_HOURS`) and names the offending row's timestamp
79
+ and hook, while the historical backlog is reported as context and never truncated. The
80
+ denominator prints on every run, so "scanned 0 rows, found 0 failures" can never be
81
+ mistaken for a clean log.
82
+
83
+ An absent log is no longer collapsed into either answer. It is corroborated against the
84
+ independent `hook_health` table the same writer populates: rows there prove failures were
85
+ recorded in this checkout, so a missing file means the evidence was destroyed and the gate
86
+ fails; no rows means there is nothing to measure and the check skips with the reason
87
+ named. Treating absence as an error — as originally specified — would have made the
88
+ nightly gate permanently red in CI, where the log is gitignored, and on every fresh
89
+ install, where it is created lazily on first failure.
90
+
91
+ Not a package change: `scripts/` is not in the published `files` array.
92
+ Plan `plan-2026-08-11-hook-failure-signal-truthfulness`.
93
+
94
+ - **The release verifier reported FAIL on a publish that had fully succeeded.** It slept two
95
+ seconds, read the npm registry once, and treated "the replica has not caught up" and "the
96
+ publish did not land" as the same answer. npm is read-after-write eventually consistent, so
97
+ both states produce the identical value.
98
+
99
+ This is the blind-gate law with the polarity flipped: usually absence reads as PASS, here it
100
+ read as FAIL — the same defect, a check that cannot distinguish *not yet* from *not ever*.
101
+ The flipped polarity is what makes it dangerous, because a false FAIL on a release tool
102
+ invites a re-publish or a version bump against a registry where versions are immutable.
103
+
104
+ Replaced with a deadline-bounded poll (90s, 3s interval, overridable) that reports how long
105
+ it waited and, on failure, states plainly that the absence is not propagation lag. The
106
+ demand itself was correct and is unchanged — `+ @massu/core@x.y.z` on stdout is not a
107
+ receipt; only the timeout was wrong.
108
+
109
+ Not a package change: `scripts/` is not in the published `files` array.
110
+
111
+ - **`require()` of a TypeScript specifier held CI red in both repos for two days.** A
112
+ `require()` is resolved by whatever loader is running — not by `tsc`, and not by the vitest
113
+ transform. Node only gained require-of-TypeScript in 22.18; `packages/core` declares
114
+ `engines.node >= 22.16.0` and CI pins exactly that floor, so three such calls in
115
+ `hooks-stdout-convention.test.ts` passed on a Node 26 dev machine and threw
116
+ `SyntaxError: Unexpected identifier 'as'` on every runner. Replaced with a static ESM import
117
+ and guarded by a new drift-guard, which is a static property of the source and so goes red on
118
+ any Node version. A fourth call site in production code was found by enumerating the tree
119
+ rather than the failure, and stays: `require()` defers evaluation while a static import is
120
+ eager, and that laziness is what keeps a database load failure from silencing the hook-failure
121
+ signal it records.
122
+
123
+ - **Publication to the public mirror now goes through a pull request.** A direct push to the
124
+ mirror's `main` cannot be gated by its required status checks — checks have not run on a
125
+ commit that does not exist yet — so every sync reported `Bypassed rule violations` and landed
126
+ regardless. That is what hid the above for two days. `scripts/ops/publish-public-mirror.sh`
127
+ derives the required contexts from the live ruleset, polls each to a deadline, treats a
128
+ cancelled or missing verdict as *not* a pass, and merges by rebase only when every one is
129
+ SUCCESS. It enforces the verdict itself rather than relying on the ruleset, because the
130
+ ruleset grants the pushing account a standing bypass.
131
+
132
+ - **A bcrypt cost factor was a wall-clock budget in disguise.** `generateApiKey()` hashes at
133
+ cost 12 — ~350ms of deliberate CPU — in three tests that do not test hashing, which blew
134
+ vitest's 5000ms timeout under the concurrent coverage run while passing in isolation. Lowering
135
+ the cost factor would have been a security regression with nothing in the tree to catch it, so
136
+ `BCRYPT_COST` is now named and the property is asserted directly: the configured cost reaches
137
+ bcrypt, with the full key and never the plaintext-stored prefix.
138
+
139
+ - **Importing any of 18 hook modules executed the hook.** A bare `main()` at module scope means
140
+ importing the module reads stdin, does its work, and exits the host process. Enumerating the
141
+ tree found 15 with no guard and 2 more guarded by a filename suffix — invisible to a sweep for
142
+ "unguarded", and disarmed silently by a rename. All 18 now share one
143
+ `isDirectInvocation(import.meta.url)` helper, proven behaviourally against the built bundles.
144
+
145
+ - **A live destructive-path defect was hiding behind a permanently-red CI job.** Three sites in a
146
+ test harness widened to the filesystem root when a variable was empty, one of them under the
147
+ memory-store tree. The guard had been reporting it correctly on every run for long enough that
148
+ its redness stopped carrying information. Destructive paths there now go through a chokepoint
149
+ that cannot express the unsafe form. (Plan
150
+ `plan-2026-08-08-incident-coverage-range-discharge`: the CR-62 linkage ratchet shipped
151
+ 2026-08-09 gains its real-history differential, which shows all 10 post-ratchet obligated
152
+ commits discharging via containment or citation and none via the legacy fallback.)
153
+
154
+ None of these are package changes: `scripts/` and `__tests__/` are not in the published
155
+ `files` array, and `website/` is not published.
156
+
157
+ - **`massu watch` could start a file watcher that was dead for its entire lifetime.** chokidar
158
+ can return a watcher that emits `ready`, emits no `error`, and then delivers nothing, ever.
159
+ Interleaved A/B in a single load window: fsevents **8 of 60** dead against **0 of 60** for the
160
+ same directories with polling (Fisher's exact ≈ 0.006). The consequence for a user is silent
161
+ staleness — `.claude/` stops refreshing, `massu watch --status` looks healthy, and nothing is
162
+ logged. Nothing in the daemon would ever have noticed: its only reconcile trigger was a tick
163
+ gap over 30s, whose predicate is *"did the clock jump?"* while the property is *"might we have
164
+ missed events?"*, and a watcher dead from birth produces no tick gap at all.
165
+
166
+ The tick now writes a sentinel the watcher itself must echo back. Silence is treated as death:
167
+ log loudly, reconcile, and after two consecutive dead intervals close and re-watch. Escalation
168
+ fires on powers of two of the dead streak, so a permanently dead watcher cannot trade a
169
+ silent-staleness bug for a full refresh plus a watcher rebuild every ten seconds forever.
170
+ Polling was rejected as the fix — it trades a correctness bug for a battery-life bug.
171
+
172
+ Two constants here are claims about the outside world and were measured rather than assumed.
173
+ The canary shares ONE recursive FSEvents stream with the watched source directories (verified
174
+ by recording the paths streams are actually opened on — a canary on its own stream could have
175
+ been alive while the source watcher was dead), and its grace floor is set from measured echo
176
+ latency: ~48ms median, but a tail to 941ms under contention.
177
+ `scripts/ops/measure-canary-echo-latency.mjs` ships so the number can be re-checked instead of
178
+ trusted. Plan `plan-2026-08-12-watch-daemon-silent-dead-watcher`.
179
+
180
+ This one IS a package change: `packages/core/src/watch/daemon.ts` and the new
181
+ `packages/core/src/watch/canary.ts` are published.
182
+
13
183
  ## [2.5.1] - 2026-08-09
14
184
 
15
185
  ### Fixed