@adrata/adrata-mcp 1.0.8 → 1.0.30

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.
@@ -13,9 +13,9 @@ const TERMINAL_STAGES = new Set(['production', 'deep backlog']);
13
13
  // has actually taken a pass and therefore require both an end-to-end owner and
14
14
  // a current handler.
15
15
  const ACTIVE_STAGES = new Set(['aligning', 'in progress', 'staging qa1', 'staging qa2']);
16
- // A card crossing the cut line into Up Next must be executable. Production is
17
- // included because historical evidence does not stop mattering after release.
18
- const EXECUTABLE_STAGES = new Set(['up next', ...ACTIVE_STAGES, 'production']);
16
+ // Acceptance and release still require executable facts, but do not require
17
+ // an active build/QA handler. Stage placement alone is not delivery evidence.
18
+ const EXECUTABLE_STAGES = new Set(['up next', ...ACTIVE_STAGES, 'ready to ship', 'production']);
19
19
 
20
20
  function normalized(value) {
21
21
  return String(value ?? '')
@@ -51,9 +51,14 @@ export function deliveryContradictions(evidence) {
51
51
 
52
52
  /**
53
53
  * @param {Array<object>} boards Full board payloads from GET /work-boards/{id}.
54
- * @param {{now?: Date|string|number}} options
54
+ * @param {{now?: Date|string|number, expectedBoards?: number}} options
55
+ * `expectedBoards` is how many boards the caller LISTED, before any of their
56
+ * detail reads failed. Pass it whenever the two numbers can differ; it is the
57
+ * only way this function can tell "every board is clean" from "most boards
58
+ * never arrived". It defaults to the number handed in, so a caller that reads
59
+ * boards by another route is not forced to invent one.
55
60
  */
56
- export function auditWorkHubBoards(boards, { now = new Date() } = {}) {
61
+ export function auditWorkHubBoards(boards, { now = new Date(), expectedBoards } = {}) {
57
62
  const nowMs = new Date(now).getTime();
58
63
  const findings = [];
59
64
  const perBoard = [];
@@ -61,13 +66,61 @@ export function auditWorkHubBoards(boards, { now = new Date() } = {}) {
61
66
  let openCards = 0;
62
67
  let activeCards = 0;
63
68
 
69
+ // A scan with nothing in it is the one result this audit must never report as
70
+ // health. `trustworthy` is derived from an empty finding list, so a workspace
71
+ // that returned no boards — or whose board reads all failed — would otherwise
72
+ // read exactly like a workspace whose every card is in order, and read that
73
+ // way in the confident words below. The repo has this failure written down
74
+ // (`scripts/check-context-stores.sh` refuses to "pass vacuously"); this is the
75
+ // same floor, in the same shape.
76
+ const expected = Number.isFinite(expectedBoards) ? expectedBoards : boards.length;
77
+ if (boards.length === 0) {
78
+ findings.push({
79
+ boardId: null,
80
+ boardName: null,
81
+ code: expected > 0 ? 'boards_unreadable' : 'no_boards_read',
82
+ itemId: null,
83
+ title: null,
84
+ stage: null,
85
+ detail:
86
+ expected > 0
87
+ ? `All ${expected} listed board(s) failed to read. This audit covered nothing.`
88
+ : 'No board was returned, so nothing was audited. An empty scan is not a clean scan.',
89
+ });
90
+ } else if (expected > boards.length) {
91
+ findings.push({
92
+ boardId: null,
93
+ boardName: null,
94
+ code: 'boards_unreadable',
95
+ itemId: null,
96
+ title: null,
97
+ stage: null,
98
+ detail: `${expected - boards.length} of ${expected} listed board(s) failed to read and were not audited. The findings below cover ${boards.length} board(s) only.`,
99
+ });
100
+ }
101
+
64
102
  for (const board of boards) {
65
103
  const columns = new Map((board.columns ?? []).map((column) => [column.id, column]));
66
104
  const boardFindings = [];
67
- const items = board.items ?? [];
105
+ // An absent `items` is NOT an empty board. Collapsing the two is how a
106
+ // partial payload comes to certify a board nobody actually read.
107
+ const itemsUnreadable = !Array.isArray(board.items);
108
+ const items = itemsUnreadable ? [] : board.items;
68
109
  totalCards += items.length;
69
110
 
70
- if (board.truncated === true) {
111
+ if (itemsUnreadable) {
112
+ boardFindings.push({
113
+ code: 'board_items_unreadable',
114
+ itemId: null,
115
+ title: null,
116
+ detail: 'This board payload carried no card list, so none of its cards were audited.',
117
+ });
118
+ }
119
+
120
+ if (
121
+ board.truncated === true ||
122
+ (!itemsUnreadable && Number.isInteger(board.itemCount) && board.itemCount > items.length)
123
+ ) {
71
124
  boardFindings.push({
72
125
  code: 'board_truncated',
73
126
  itemId: null,
@@ -89,13 +142,19 @@ export function auditWorkHubBoards(boards, { now = new Date() } = {}) {
89
142
  const add = (code, detail) =>
90
143
  boardFindings.push({ code, itemId: item.id, title: item.title, stage, detail });
91
144
 
145
+ if (!column || !normalized(column.name)) {
146
+ add(
147
+ 'card_stage_unreadable',
148
+ 'The card has no resolvable stage, so its readiness requirements could not be audited.'
149
+ );
150
+ }
92
151
  if (mustBeExecutable && !String(item.body ?? '').trim()) {
93
152
  add('missing_body', 'The card crossed the cut line without working context.');
94
153
  }
95
154
  if (mustBeExecutable) {
96
155
  if (item.criteria?.total === 0) {
97
156
  add('missing_acceptance_criteria', 'Nobody has stated a checkable definition of done.');
98
- } else if (item.criteria === undefined) {
157
+ } else if (!Number.isInteger(item.criteria?.total) || item.criteria.total < 0) {
99
158
  add('criteria_not_measured', 'This read did not include acceptance-criteria status.');
100
159
  }
101
160
  if (!item.kind)
@@ -142,7 +201,13 @@ export function auditWorkHubBoards(boards, { now = new Date() } = {}) {
142
201
  for (const finding of findings) counts[finding.code] = (counts[finding.code] ?? 0) + 1;
143
202
 
144
203
  const priorityOrder = [
204
+ // Coverage first, always. A finding about a card is worth reading only once
205
+ // you know which cards were in the scan at all.
206
+ 'no_boards_read',
207
+ 'boards_unreadable',
208
+ 'board_items_unreadable',
145
209
  'board_truncated',
210
+ 'card_stage_unreadable',
146
211
  'unowned_active_card',
147
212
  'unhandled_active_pass',
148
213
  'missing_acceptance_criteria',
@@ -158,6 +223,7 @@ export function auditWorkHubBoards(boards, { now = new Date() } = {}) {
158
223
  trustworthy: findings.length === 0,
159
224
  generatedAt: new Date(nowMs).toISOString(),
160
225
  boards: boards.length,
226
+ boardsExpected: expected,
161
227
  totalCards,
162
228
  openCards,
163
229
  activeCards,
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Can a second person falsify this criterion?
3
+ *
4
+ * # Why this exists
5
+ *
6
+ * A Starfield criterion is four fields — where, given, when, then — and the
7
+ * server checks exactly three things about each part: it is not blank, it is
8
+ * under 600 characters, and it is not a pasted transcript
9
+ * (`criteria/edit.rs::validate_part`). Every one of those is a check on the
10
+ * STRING. None of them is a check on whether the sentence says something a
11
+ * reviewer could disagree with.
12
+ *
13
+ * So `thenText: "it works correctly"` is a valid criterion today. It will be
14
+ * ticked, it will count toward `met`, and at the QA gate it will be graded on
15
+ * WHO ticked it and in WHICH dwell — both of which can be perfectly in order
16
+ * while the sentence itself remains unfalsifiable. The independence machinery
17
+ * in `criteria/rule.rs` is strong and this is the gap underneath it: it
18
+ * guarantees that a different pair of hands checked the criterion, not that the
19
+ * criterion could have failed.
20
+ *
21
+ * The rubric this borrows from is the repo's own `task-validation` skill
22
+ * (`.claude/skills/task-validation/references/task-rubrics.md`, metric 6,
23
+ * "Acceptance Criteria Quality"), which names the failure directly:
24
+ *
25
+ * BAD (Score: 50):
26
+ * - "Button looks good" (subjective)
27
+ * - "Works correctly" (vague)
28
+ * - "Performance is acceptable" (unmeasurable)
29
+ *
30
+ * That rubric has never run: it scores YAML task files by hand and is wired to
31
+ * no command, no hook and no CI job. What follows is the checkable third of it,
32
+ * moved to the place where criteria are actually written.
33
+ *
34
+ * # What it deliberately does NOT do
35
+ *
36
+ * It does not score. `audit_work_hub` says in its own description that it "does
37
+ * not award a vanity score", and that judgement holds here: a criterion graded
38
+ * 72/100 tells a reader nothing they can act on, and the only recorded run of
39
+ * the SPOQ rubric in this repo scored nine consecutive tasks 98/100 — a number
40
+ * that had stopped distinguishing between them. Findings are named or absent.
41
+ *
42
+ * It does not block. `describeMissingBody` states the argument already: "a
43
+ * refused capture is how work stops reaching the board at all." These are
44
+ * advisory notes returned beside a dry-run preview and beside a criteria read,
45
+ * where a person or an agent is already deciding what to write.
46
+ *
47
+ * # Why the patterns are anchored
48
+ *
49
+ * Every rule below matches a WHOLE normalised field, or a whole word for
50
+ * placeholders. A substring rule would fire on "the row saves successfully and
51
+ * the toast names the new owner", which is a good criterion containing a weak
52
+ * word. The cost of that false positive is an author learning to ignore the
53
+ * notes, which is worse than the check not existing. When in doubt these rules
54
+ * stay silent.
55
+ */
56
+
57
+ /** Lowercase, collapse whitespace, drop trailing punctuation. */
58
+ function normalise(value) {
59
+ return String(value ?? '')
60
+ .trim()
61
+ .toLowerCase()
62
+ .replace(/\s+/g, ' ')
63
+ .replace(/[.!]+$/, '');
64
+ }
65
+
66
+ // The article is optional because the rubric's own worked example is the
67
+ // article-less form — "Button looks good" — and an author writing a weak
68
+ // criterion is exactly the author who drops it.
69
+ const SUBJECT =
70
+ '(?:it|this|(?:the )?(?:app|feature|page|screen|button|toggle|flow|ui|component|form|list|modal|panel|view|result|change|fix|thing))';
71
+
72
+ /**
73
+ * A `then` that asserts nothing observable.
74
+ *
75
+ * Anchored whole-string, so "the toggle works" fires and "the toggle works on a
76
+ * cold reload and the label reads Dark" does not.
77
+ */
78
+ const UNFALSIFIABLE_THEN = [
79
+ new RegExp(
80
+ `^${SUBJECT}? ?(?:works|work|is working|works correctly|works properly|works as expected|functions correctly|functions as expected)$`
81
+ ),
82
+ new RegExp(
83
+ `^${SUBJECT}? ?(?:behaves|performs|renders|displays|loads|responds) (?:correctly|properly|as expected|fine|ok|okay)$`
84
+ ),
85
+ new RegExp(
86
+ `^${SUBJECT}? ?(?:is|looks|seems) (?:good|right|correct|fine|ok|okay|acceptable|reasonable|as expected|better|improved)$`
87
+ ),
88
+ new RegExp(`^${SUBJECT}? ?(?:is|are)? ?(?:successful|success|succeeds|passes|passed)$`),
89
+ /^(?:everything|all of it|all) (?:works|is fine|is ok|is okay|is correct|looks good)$/,
90
+ /^no (?:errors|issues|problems|regressions|bugs)(?: (?:occur|appear|are seen|are observed|are present))?$/,
91
+ /^(?:performance|it) (?:is|feels) (?:acceptable|fast|good|fine|snappy|better|improved)$/,
92
+ /^(?:there (?:is|are) )?no (?:visual )?(?:change|changes|difference|differences)$/,
93
+ ];
94
+
95
+ /**
96
+ * A `where` that names no surface.
97
+ *
98
+ * The field asks for a "Surface, environment, account, or role". A bare
99
+ * environment ("staging", "production") is a legitimate answer and is NOT
100
+ * listed here; a whole-product noun is not an answer at all, because it does
101
+ * not tell the next reviewer where to stand.
102
+ */
103
+ const UNLOCATED_WHERE = [
104
+ /^(?:the )?(?:app|application|product|system|platform|site|website|service|ui|frontend|front end|backend|back end|client|server)$/,
105
+ /^(?:the )?(?:code|codebase|repo|repository|source)$/,
106
+ // `n/a` and friends are deliberately absent: PLACEHOLDER already names them,
107
+ // and one field earning two findings for one word reads as the check
108
+ // stuttering rather than as two problems.
109
+ /^(?:it|this|there|here|everywhere|anywhere|all surfaces|any surface|none|various)$/,
110
+ ];
111
+
112
+ /** Text nobody has written yet, left in a field that is required to be written. */
113
+ // WIP limit(s) names the board feature, not unfinished prose. Exempt only that
114
+ // noun phrase; bare WIP and separate placeholders in the same field still fire.
115
+ const PLACEHOLDER =
116
+ /(?:^|[^a-z0-9])(?:tbd|tba|todo|fixme|wip(?![\s-]+limits?\b)|xxx|placeholder|n\/a)(?:[^a-z0-9]|$)|\?{3,}|<[^>]{0,40}>/i;
117
+
118
+ const PART_LABELS = {
119
+ whereText: 'where',
120
+ givenText: 'given',
121
+ whenText: 'when',
122
+ thenText: 'then',
123
+ };
124
+
125
+ /**
126
+ * Inspect one criterion. Returns zero or more findings; never throws.
127
+ *
128
+ * @param {{whereText?: string, givenText?: string, whenText?: string, thenText?: string}} criterion
129
+ * @param {{ordinal?: number, id?: string}} context
130
+ */
131
+ export function inspectCriterion(criterion, { ordinal = null, id = null } = {}) {
132
+ const findings = [];
133
+ const at = { criterionId: id, ordinal };
134
+
135
+ const thenText = normalise(criterion?.thenText);
136
+ if (thenText && UNFALSIFIABLE_THEN.some((pattern) => pattern.test(thenText))) {
137
+ findings.push({
138
+ ...at,
139
+ code: 'unfalsifiable_then',
140
+ field: 'thenText',
141
+ text: String(criterion.thenText).trim(),
142
+ detail:
143
+ 'The observable result asserts nothing a reviewer could disagree with, so this criterion cannot fail. Name what is on the screen or in the response — a value, a count, a state, an exact message.',
144
+ });
145
+ }
146
+
147
+ const whereText = normalise(criterion?.whereText);
148
+ if (whereText && UNLOCATED_WHERE.some((pattern) => pattern.test(whereText))) {
149
+ findings.push({
150
+ ...at,
151
+ code: 'unlocated_where',
152
+ field: 'whereText',
153
+ text: String(criterion.whereText).trim(),
154
+ detail:
155
+ 'This names the whole product rather than a place to stand. Give the surface, environment, account, or role the next reviewer should check it on.',
156
+ });
157
+ }
158
+
159
+ for (const [field, label] of Object.entries(PART_LABELS)) {
160
+ const raw = criterion?.[field];
161
+ if (typeof raw === 'string' && raw.trim() && PLACEHOLDER.test(raw)) {
162
+ findings.push({
163
+ ...at,
164
+ code: 'placeholder_text',
165
+ field,
166
+ text: raw.trim(),
167
+ detail: `The ${label} clause still carries a placeholder. A criterion nobody has finished writing cannot be executed by somebody who did not write the code.`,
168
+ });
169
+ }
170
+ }
171
+
172
+ return findings;
173
+ }
174
+
175
+ /**
176
+ * Inspect a whole list.
177
+ *
178
+ * The `vacuous` flag exists because `clean` is derived from an empty finding
179
+ * list, and an empty INPUT produces an empty finding list too. A caller that
180
+ * read `clean` alone would report a card with no criteria at all as a card
181
+ * whose criteria are in order — the same false green the board audit's
182
+ * coverage floor exists to stop.
183
+ *
184
+ * @param {Array<object>} criteria
185
+ */
186
+ export function inspectCriteria(criteria) {
187
+ const list = Array.isArray(criteria) ? criteria : [];
188
+ const findings = [];
189
+ for (const [index, criterion] of list.entries()) {
190
+ findings.push(
191
+ ...inspectCriterion(criterion, { ordinal: index + 1, id: criterion?.id ?? null })
192
+ );
193
+ }
194
+ return {
195
+ checked: list.length,
196
+ vacuous: list.length === 0,
197
+ clean: list.length > 0 && findings.length === 0,
198
+ findings,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * The one-line advisory a tool puts beside its own payload, or `undefined` when
204
+ * there is nothing to say. `undefined` rather than a cheerful "all good" so the
205
+ * note only ever appears when it carries information.
206
+ */
207
+ export function describeCriteriaQuality(criteria) {
208
+ const report = inspectCriteria(criteria);
209
+ if (report.vacuous || report.clean) return undefined;
210
+ const codes = [...new Set(report.findings.map((finding) => finding.code))].join(', ');
211
+ return `${report.findings.length} of ${report.checked} criterion check(s) look unexecutable (${codes}). These are advisory, not a refusal: see \`criteriaQuality.findings\`. A criterion that cannot fail is not a definition of done.`;
212
+ }
@@ -7,6 +7,34 @@
7
7
 
8
8
  import { z } from 'zod';
9
9
  import { md, mdError, table, formatDate } from '../output-formatter.js';
10
+ import { validateApiBridgeRequest } from '../api-bridge.js';
11
+ import { governedWriteArgs, previewMarkdown } from '../governance/governed-args.js';
12
+
13
+ // The real handlers create a fresh row before enqueueing. A failed enqueue
14
+ // returns non-2xx, which the shared middleware does not cache as completed;
15
+ // its 24h in-progress reservation still blocks same-key retries. Neither
16
+ // handler has durable request/job deduplication after expiry or a new key.
17
+ // Do not turn the old dead routes into outbound side effects until that
18
+ // failed-first-attempt contract is repaired and independently replayed.
19
+ const REPLAY_BLOCKER = 'Live calls and SMS are unavailable through this tool until durable request/job replay is verified. '
20
+ + 'The API inserts a record before queueing; an enqueue failure can leave a partial result and a 24-hour in-progress key. '
21
+ + 'A new or expired key can duplicate that work. Nothing was sent; do not bypass this hold with the raw API bridge.';
22
+
23
+ function previewCommunication(args, heading, request) {
24
+ try {
25
+ const validation = validateApiBridgeRequest({ ...args, method: request.method, path: request.path });
26
+ if (validation.dryRun) {
27
+ return md(previewMarkdown(heading, {
28
+ ...validation, wouldSend: false, body: request.body, notes: request.notes, blocked: true, reason: REPLAY_BLOCKER,
29
+ fix: 'Backend durable request/job replay and independent failed-first-attempt verification are required.',
30
+ note: 'Approval fields do not lift this safety hold. This tool currently provides previews only.',
31
+ }, { executionAvailable: false }));
32
+ }
33
+ return { ...mdError(`${heading} unavailable`, REPLAY_BLOCKER), isError: true };
34
+ } catch (err) {
35
+ return { ...mdError(`${heading} refused`, err.message), isError: true };
36
+ }
37
+ }
10
38
 
11
39
  export function register(server, api, AUTH) {
12
40
 
@@ -15,52 +43,19 @@ export function register(server, api, AUTH) {
15
43
  // -----------------------------------------------------------------------
16
44
  server.tool(
17
45
  'make_call',
18
- 'Dial a number via connected telephony provider. Initiates the call and returns a call ID for tracking. Use get_call_transcript after the call to get the AI summary.',
46
+ 'Preview a call request. Live execution is held until durable request/job replay is verified; approval does not bypass the hold.',
19
47
  {
20
48
  phoneNumber: z.string().describe('Phone number to dial'),
21
49
  personId: z.string().optional().describe('Person ID for CRM linking'),
22
50
  companyId: z.string().optional().describe('Company ID for CRM linking'),
23
- notes: z.string().optional().describe('Pre-call notes or talking points'),
51
+ notes: z.string().optional().describe('Pre-call notes shown in this preview only; not persisted or sent to the calling API'),
52
+ ...governedWriteArgs(z),
24
53
  },
25
- async (args) => {
26
- try {
27
- const result = await api('POST', '/api/v1/calls', {
28
- body: {
29
- phoneNumber: args.phoneNumber,
30
- personId: args.personId,
31
- companyId: args.companyId,
32
- notes: args.notes,
33
- },
34
- });
35
-
36
- const c = result?.data || result || {};
37
- let text = `## Call Initiated\n\n`;
38
- text += `- **To:** ${args.phoneNumber}\n`;
39
- text += `- **Call ID:** ${c.id || c.callId || '\u2014'}\n`;
40
- text += `- **Status:** ${c.status || 'Connecting...'}\n`;
41
- if (args.personId) text += `- **Linked to person:** ${args.personId}\n`;
42
- if (args.notes) text += `\n### Talking Points\n${args.notes}\n`;
43
- text += '\nAfter the call, use `get_call_transcript` to get the AI summary.\n';
44
-
45
- // Log action
46
- if (args.personId) {
47
- await api('POST', '/api/v1/actions', {
48
- body: {
49
- title: `Call to ${args.phoneNumber}`,
50
- type: 'call',
51
- personId: args.personId,
52
- companyId: args.companyId,
53
- status: 'IN_PROGRESS',
54
- metadata: { callId: c.id || c.callId, phoneNumber: args.phoneNumber },
55
- },
56
- }).catch(() => {});
57
- }
58
-
59
- return md(text);
60
- } catch (err) {
61
- return mdError('Call failed', err.message);
62
- }
63
- }
54
+ async (args) => previewCommunication(args, 'Call request', {
55
+ method: 'POST', path: '/api/v1/calling/dial',
56
+ body: { phoneNumber: args.phoneNumber, personId: args.personId, companyId: args.companyId },
57
+ notes: args.notes,
58
+ })
64
59
  );
65
60
 
66
61
  // -----------------------------------------------------------------------
@@ -68,34 +63,17 @@ export function register(server, api, AUTH) {
68
63
  // -----------------------------------------------------------------------
69
64
  server.tool(
70
65
  'send_sms',
71
- 'Send a text message via connected telephony provider (Twilio/Vonage). Links to CRM for tracking.',
66
+ 'Preview an SMS request. Live execution is held until durable request/job replay is verified; approval does not bypass the hold.',
72
67
  {
73
68
  to: z.string().describe('Recipient phone number'),
74
69
  message: z.string().describe('SMS message text'),
75
70
  personId: z.string().optional().describe('Person ID for CRM linking'),
71
+ ...governedWriteArgs(z),
76
72
  },
77
- async (args) => {
78
- try {
79
- const result = await api('POST', '/api/v1/sms', {
80
- body: {
81
- to: args.to,
82
- message: args.message,
83
- personId: args.personId,
84
- },
85
- });
86
-
87
- const r = result?.data || result || {};
88
- let text = `## SMS Sent\n\n`;
89
- text += `- **To:** ${args.to}\n`;
90
- text += `- **Message:** ${args.message}\n`;
91
- text += `- **Status:** ${r.status || 'Sent'}\n`;
92
- if (r.id) text += `- **ID:** ${r.id}\n`;
93
-
94
- return md(text);
95
- } catch (err) {
96
- return mdError('SMS failed', err.message);
97
- }
98
- }
73
+ async (args) => previewCommunication(args, 'SMS request', {
74
+ method: 'POST', path: '/api/v1/sms/send',
75
+ body: { toNumber: args.to, body: args.message, personId: args.personId },
76
+ })
99
77
  );
100
78
 
101
79
  // -----------------------------------------------------------------------
@@ -182,8 +182,17 @@ export function describeCount({ found, of, noun, ofNoun, shapeRecognized = true,
182
182
  typeof of === 'number'
183
183
  ? `${found} of ${of} ${ofNoun ?? noun}`
184
184
  : `${found} ${noun}${truncated ? ' (page limit reached — more exist)' : ''}`;
185
+ // Three DIFFERENT zeros, and they lead a reader to three different actions. The first two
186
+ // used to be exchanged: a zero measured against rows elsewhere announced the workspace was
187
+ // empty, and a zero measured against a stated zero denominator announced there was no
188
+ // denominator. Each contradicted the count printed immediately before it, which is why
189
+ // neither was caught — the number was right and only the sentence was wrong, and the
190
+ // sentence is the part that gets acted on.
185
191
  if (found === 0 && typeof of === 'number' && of > 0) {
186
- return `${base}. The store is reachable and genuinely empty for this workspace this is a real zero, not a failed read.`;
192
+ return `${base}. The store does hold ${of} ${ofNoun ?? noun} none of them match this scope. This is a real zero for this filter, NOT an empty store; widen the scope rather than reporting that there is nothing.`;
193
+ }
194
+ if (found === 0 && of === 0) {
195
+ return `${base}. The denominator was read and it is zero: the store is reachable and holds nothing for this workspace at all. This is an UNWRITTEN store, not evidence that the thing being counted does not happen.`;
187
196
  }
188
197
  if (found === 0) {
189
198
  return `${base}. Zero rows AND no denominator available, so this cannot distinguish an empty workspace from an undeployed store — say so rather than reporting "none".`;