@rubytech/create-realagent-code 0.1.615 → 0.1.616
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 +1 -1
- package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.d.ts +2 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.d.ts.map +1 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.js +291 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/__tests__/write-llms-txt.test.js.map +1 -0
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts +13 -1
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js +127 -18
- package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js.map +1 -1
- package/payload/platform/lib/aeo-llms-txt-writer/src/__tests__/write-llms-txt.test.ts +391 -0
- package/payload/platform/lib/aeo-llms-txt-writer/src/index.ts +156 -19
- package/payload/platform/lib/aeo-llms-txt-writer/vitest.config.ts +10 -0
- package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +15 -1
- package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-spawn-tee/dist/index.js +129 -7
- package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-spawn-tee/src/__tests__/spawn-tee.test.ts +227 -0
- package/payload/platform/lib/mcp-spawn-tee/src/index.ts +133 -6
- package/payload/platform/plugins/admin/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/admin/mcp/dist/index.js +11 -3
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts +3 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js +38 -20
- package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js.map +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +6 -5
- package/payload/platform/plugins/admin/skills/publish-site/SKILL.md +3 -0
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +8 -0
- package/payload/platform/plugins/aeo/PLUGIN.md +1 -1
- package/payload/platform/plugins/aeo/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/aeo/mcp/dist/index.js +17 -10
- package/payload/platform/plugins/aeo/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/browser/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/connector/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/contacts/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/dispatch/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/docs/references/aeo.md +4 -3
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/email/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/google/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/graph-viewer/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/inventory/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/ledger/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/memory/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/outlook/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/replicate/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/scheduling/PLUGIN.md +3 -4
- package/payload/platform/plugins/scheduling/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +0 -6
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js +65 -20
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.js +20 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts +12 -4
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +39 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js +21 -6
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +26 -5
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.js +17 -51
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +6 -4
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +113 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +8 -18
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +14 -27
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +125 -56
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
- package/payload/platform/plugins/storage-broker/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/url-get/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/voice-mirror/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/whatsapp/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/work/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/plugins/workflows/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/platform/services/claude-session-manager/dist/index.js +17 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.d.ts +80 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.js +201 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-helper-census.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/buyers/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/premium-plugins/real-agent/plugins/estate-sales/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/premium-plugins/real-agent/plugins/loop/PLUGIN.md +12 -0
- package/payload/premium-plugins/real-agent/plugins/loop/lib/mcp-spawn-tee/index.js +129 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js +13 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/field-census.d.ts +22 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/field-census.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/field-census.js +81 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/field-census.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.d.ts +17 -5
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.js +90 -17
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-schema-keys.d.ts +2 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-schema-keys.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-schema-keys.js +1010 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-schema-keys.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.js +2 -2
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/customer-preferences.js +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/customer-preferences.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/feedback.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/feedback.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/feedback.js +5 -5
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/feedback.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-match-batch.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-match-batch.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-match-batch.js +2 -2
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-match-batch.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-match.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-match.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-match.js +4 -4
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-match.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/people-detail.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/people-detail.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/people-detail.js +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/people-detail.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/people-search.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/people-search.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/people-search.js +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/people-search.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-detail.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-detail.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-detail.js +13 -6
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-detail.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.js +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-search.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-search.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-search.js +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-search.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.js +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/supplier.js +4 -4
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/supplier.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/team-availability.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/team-availability.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/team-availability.js +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/team-availability.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/team-info.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/team-info.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/team-info.js +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/team-info.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/viewing-detail.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/viewing-detail.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/viewing-detail.js +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/viewing-detail.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/viewing-search.d.ts +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/viewing-search.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/viewing-search.js +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/viewing-search.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/package.json +2 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/scripts/generate-loop-schema-keys.mjs +87 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/new-tool-shape.test.ts +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/read-fidelity.test.ts +303 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/swagger-write-coverage.test.ts +6 -6
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/index.ts +13 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/field-census.ts +86 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/loop-api.ts +116 -17
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/loop-schema-keys.ts +1010 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/auto-responder.ts +2 -2
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/customer-preferences.ts +3 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/feedback.ts +6 -4
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/marketing-match-batch.ts +4 -2
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/marketing-match.ts +6 -4
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/people-detail.ts +5 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/people-search.ts +5 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-detail.ts +17 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-listed.ts +5 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-search.ts +5 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-sold.ts +5 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/supplier.ts +4 -4
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/team-availability.ts +5 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/team-info.ts +6 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/viewing-detail.ts +5 -3
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/viewing-search.ts +5 -3
- package/payload/premium-plugins/real-agent/plugins/property-data/lib/mcp-spawn-tee/index.js +129 -7
package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/read-fidelity.test.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read fidelity and API version provenance (Task 2792).
|
|
3
|
+
*
|
|
4
|
+
* Two defects these tests pin:
|
|
5
|
+
*
|
|
6
|
+
* 1. A prose formatter guards on truthiness, so a value of 0, false, "" or
|
|
7
|
+
* "None" renders identically to a field nobody entered. A pre-flight that
|
|
8
|
+
* must chase a missing service charge cannot be built on that.
|
|
9
|
+
* 2. Nothing named the API version, so a capability audit recorded "we are on
|
|
10
|
+
* v1" against a client that has always been v2.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import { readFileSync, rmSync, statSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { resolve, dirname, join } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { LOOP_API_VERSION, formatAggregationResult, formatSingleTeamRead } from '../lib/loop-api.js';
|
|
20
|
+
import type { AggregationResult } from '../lib/loop-api.js';
|
|
21
|
+
import { LOOP_SCHEMA_KEYS } from '../lib/loop-schema-keys.js';
|
|
22
|
+
import {
|
|
23
|
+
censusRecords,
|
|
24
|
+
falsyPresentKeys,
|
|
25
|
+
matchPathPattern,
|
|
26
|
+
normalisePath,
|
|
27
|
+
wireKeys,
|
|
28
|
+
} from '../lib/field-census.js';
|
|
29
|
+
|
|
30
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
|
|
32
|
+
function emptyResult<T>(over: Partial<AggregationResult<T>> = {}): AggregationResult<T> {
|
|
33
|
+
return { results: [], failures: [], skipped: [], totalTeams: 0, ...over };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('API version provenance', () => {
|
|
37
|
+
it('matches the vendored swagger snapshot', () => {
|
|
38
|
+
const snapshot = JSON.parse(
|
|
39
|
+
readFileSync(resolve(__dirname, 'loop-swagger.snapshot.json'), 'utf-8'),
|
|
40
|
+
) as { info: { title: string } };
|
|
41
|
+
// "Loop API V2 Documentation" -> v2
|
|
42
|
+
const fromSnapshot = /\bV(\d+)\b/.exec(snapshot.info.title);
|
|
43
|
+
expect(fromSnapshot, 'snapshot info.title carries no version token').not.toBeNull();
|
|
44
|
+
expect(LOOP_API_VERSION).toBe(`v${fromSnapshot![1]}`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('names the version on the no-teams branch', () => {
|
|
48
|
+
const out = formatAggregationResult(emptyResult(), () => '', 'properties');
|
|
49
|
+
expect(out).toContain(LOOP_API_VERSION);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('names the version on the all-failed branch', () => {
|
|
53
|
+
const out = formatAggregationResult(
|
|
54
|
+
emptyResult({ totalTeams: 1, failures: [{ teamName: 'A', reason: 'boom', kind: 'other' }] }),
|
|
55
|
+
() => '',
|
|
56
|
+
'properties',
|
|
57
|
+
);
|
|
58
|
+
expect(out).toContain(LOOP_API_VERSION);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('names the version on the no-results branch', () => {
|
|
62
|
+
const out = formatAggregationResult(
|
|
63
|
+
emptyResult({ totalTeams: 1, results: [{ teamName: 'A', data: [] }] }),
|
|
64
|
+
() => '',
|
|
65
|
+
'properties',
|
|
66
|
+
);
|
|
67
|
+
expect(out).toContain(LOOP_API_VERSION);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('names the version on the records branch', () => {
|
|
71
|
+
const out = formatAggregationResult(
|
|
72
|
+
emptyResult({ totalTeams: 1, results: [{ teamName: 'A', data: [{ id: 1 }] }] }),
|
|
73
|
+
(r: { id: number }) => `- ${r.id}`,
|
|
74
|
+
'properties',
|
|
75
|
+
);
|
|
76
|
+
expect(out).toContain(LOOP_API_VERSION);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('generated schema key map', () => {
|
|
81
|
+
const MAP_PATH = resolve(__dirname, '../lib/loop-schema-keys.ts');
|
|
82
|
+
const GENERATOR = resolve(__dirname, '../../scripts/generate-loop-schema-keys.mjs');
|
|
83
|
+
|
|
84
|
+
it('is byte-identical to what the generator produces from the current snapshot', () => {
|
|
85
|
+
// Written to a temp path, never over the committed map: a check that
|
|
86
|
+
// destroys the file it is checking cannot report what it found.
|
|
87
|
+
const tmp = join(tmpdir(), `loop-schema-keys-${process.pid}.ts`);
|
|
88
|
+
try {
|
|
89
|
+
execFileSync('node', [GENERATOR], {
|
|
90
|
+
stdio: 'pipe',
|
|
91
|
+
env: { ...process.env, LOOP_SCHEMA_KEYS_OUT: tmp },
|
|
92
|
+
});
|
|
93
|
+
expect(readFileSync(tmp, 'utf-8')).toBe(readFileSync(MAP_PATH, 'utf-8'));
|
|
94
|
+
} finally {
|
|
95
|
+
rmSync(tmp, { force: true });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('carries the property detail path with the fields the audit called absent', () => {
|
|
100
|
+
const keys = LOOP_SCHEMA_KEYS['/property/residential/sales/{id}'];
|
|
101
|
+
expect(keys, 'sales property detail path missing from map').toBeDefined();
|
|
102
|
+
for (const field of ['tenure', 'serviceCharge', 'groundRent', 'saleBy', 'amlVerified']) {
|
|
103
|
+
expect(keys).toContain(field);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('stays small enough for the runtime to import', () => {
|
|
108
|
+
// The runtime imports this module on every read; the 711KB snapshot it
|
|
109
|
+
// derives from is a test fixture and must never be what gets parsed.
|
|
110
|
+
// (Whether the bundle also copies __tests__ into the payload is Task 2795
|
|
111
|
+
// and does not bear on this.)
|
|
112
|
+
expect(statSync(MAP_PATH).size).toBeLessThan(100_000);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('raw mode', () => {
|
|
117
|
+
const result = {
|
|
118
|
+
results: [{ teamName: 'A', data: [{ id: 1, serviceCharge: 0, undeclaredByAnyInterface: 'x' }] }],
|
|
119
|
+
failures: [],
|
|
120
|
+
skipped: [],
|
|
121
|
+
totalTeams: 1,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
it('emits keys the prose formatter drops', () => {
|
|
125
|
+
const out = formatAggregationResult(result, () => '- prose only', 'properties', { raw: true });
|
|
126
|
+
expect(out).toContain('serviceCharge');
|
|
127
|
+
expect(out).toContain('0');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('emits a key no interface declares', () => {
|
|
131
|
+
// Raw serialises the wire payload, not the TypeScript interface.
|
|
132
|
+
const out = formatAggregationResult(result, () => '- prose only', 'properties', { raw: true });
|
|
133
|
+
expect(out).toContain('undeclaredByAnyInterface');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('leaves default output as prose', () => {
|
|
137
|
+
const out = formatAggregationResult(result, () => '- prose only', 'properties');
|
|
138
|
+
expect(out).toContain('- prose only');
|
|
139
|
+
expect(out).not.toContain('undeclaredByAnyInterface');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('still names the version in raw mode', () => {
|
|
143
|
+
const out = formatAggregationResult(result, () => '- prose only', 'properties', { raw: true });
|
|
144
|
+
expect(out).toContain(LOOP_API_VERSION);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('every Loop read names its API version', () => {
|
|
149
|
+
// The coverage set is "tools that read from Loop", not "tools routed through
|
|
150
|
+
// formatAggregationResult". Defining it the second way is what left four
|
|
151
|
+
// reads behind: the withTeamKey tools format their own output and named no
|
|
152
|
+
// version at all, and a test enumerating only the twelve stayed green over
|
|
153
|
+
// the gap.
|
|
154
|
+
const SINGLE_TEAM_READS = [
|
|
155
|
+
{ tool: 'loop-feedback-get', source: 'tools/feedback.ts' },
|
|
156
|
+
{ tool: 'loop-auto-responder', source: 'tools/auto-responder.ts' },
|
|
157
|
+
{ tool: 'loop-supplier', source: 'tools/supplier.ts' },
|
|
158
|
+
{ tool: 'loop-customer-preferences', source: 'tools/customer-preferences.ts' },
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
it.each(SINGLE_TEAM_READS)('$tool stamps the version on its read output', ({ source }) => {
|
|
162
|
+
const src = readFileSync(resolve(__dirname, '..', source), 'utf-8');
|
|
163
|
+
expect(src, `${source} returns un-versioned output`).toContain('formatSingleTeamRead(');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('leaves no read returning bare JSON.stringify output', () => {
|
|
167
|
+
for (const { source } of SINGLE_TEAM_READS) {
|
|
168
|
+
const src = readFileSync(resolve(__dirname, '..', source), 'utf-8');
|
|
169
|
+
expect(src, `${source} still returns an unstamped payload`).not.toMatch(
|
|
170
|
+
/return JSON\.stringify\(result, null, 2\);/,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('stamps the version in both raw and prose form', () => {
|
|
176
|
+
expect(formatSingleTeamRead({ a: 0 }, 'prose')).toContain(LOOP_API_VERSION);
|
|
177
|
+
expect(formatSingleTeamRead({ a: 0 }, 'prose', { raw: true })).toContain(LOOP_API_VERSION);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('shows a zero in raw form that the prose dropped', () => {
|
|
181
|
+
const raw = formatSingleTeamRead({ rating: 0 }, 'no rating given', { raw: true });
|
|
182
|
+
expect(raw).toContain('rating');
|
|
183
|
+
expect(raw).toContain('0');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('raw parameter coverage', () => {
|
|
188
|
+
// Every read routed through formatAggregationResult must offer the mode.
|
|
189
|
+
// A read that cannot return its raw field set is a read the pre-flight
|
|
190
|
+
// cannot trust, and the gap would be silent without this.
|
|
191
|
+
const READ_TOOLS = [
|
|
192
|
+
'loop-feedback-get',
|
|
193
|
+
'loop-property-detail',
|
|
194
|
+
'loop-property-search',
|
|
195
|
+
'loop-property-listed',
|
|
196
|
+
'loop-property-sold',
|
|
197
|
+
'loop-people-detail',
|
|
198
|
+
'loop-people-search',
|
|
199
|
+
'loop-viewing-detail',
|
|
200
|
+
'loop-viewing-search',
|
|
201
|
+
'loop-marketing-match',
|
|
202
|
+
'loop-marketing-match-batch',
|
|
203
|
+
'loop-team-info',
|
|
204
|
+
'loop-team-availability',
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
it.each(READ_TOOLS)('%s declares the raw parameter', (tool) => {
|
|
208
|
+
const src = readFileSync(resolve(__dirname, '../index.ts'), 'utf-8');
|
|
209
|
+
const start = src.indexOf(`"${tool}"`);
|
|
210
|
+
expect(start, `${tool} not registered`).toBeGreaterThan(-1);
|
|
211
|
+
// Bound on this tool's own registration, not a character count: a fixed
|
|
212
|
+
// window spills into the next lifelineTool call and a missing declaration
|
|
213
|
+
// reads as present because its neighbour has one.
|
|
214
|
+
const next = src.indexOf('lifelineTool(server,', start);
|
|
215
|
+
const block = src.slice(start, next === -1 ? src.length : next);
|
|
216
|
+
expect(block, `${tool} has no raw parameter`).toContain('raw: z.boolean().optional()');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('property detail field fidelity', () => {
|
|
221
|
+
// The renderer is a closure inside propertyDetail, so the guards are
|
|
222
|
+
// asserted at their source. They are the defect.
|
|
223
|
+
const SRC_PATH = resolve(__dirname, '../tools/property-detail.ts');
|
|
224
|
+
|
|
225
|
+
it('does not gate the money fields on truthiness', () => {
|
|
226
|
+
const src = readFileSync(SRC_PATH, 'utf-8');
|
|
227
|
+
expect(src, 'serviceCharge still behind a truthiness guard').not.toContain(
|
|
228
|
+
'if (p.serviceCharge)',
|
|
229
|
+
);
|
|
230
|
+
expect(src, 'groundRent still behind a truthiness guard').not.toContain('if (p.groundRent)');
|
|
231
|
+
expect(src, 'tenure still dropped when None').not.toContain('p.tenure !== "None"');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('declares and renders the two fields the audit called unavailable', () => {
|
|
235
|
+
const src = readFileSync(SRC_PATH, 'utf-8');
|
|
236
|
+
expect(src, 'sale method not declared').toContain('saleBy');
|
|
237
|
+
expect(src, 'money-laundering check status not declared').toContain('amlVerified');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('field census primitives', () => {
|
|
242
|
+
it('strips the query string before matching', () => {
|
|
243
|
+
expect(normalisePath('/property/residential/sales?MinPrice=1')).toBe(
|
|
244
|
+
'/property/residential/sales',
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('matches a live id-bearing path to its swagger pattern', () => {
|
|
249
|
+
const patterns = ['/property/residential/sales/{id}', '/property/residential/sales'];
|
|
250
|
+
expect(matchPathPattern('/property/residential/sales/4821', patterns)).toBe(
|
|
251
|
+
'/property/residential/sales/{id}',
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('prefers the literal pattern over the templated one', () => {
|
|
256
|
+
const patterns = ['/property/residential/sales/{id}', '/property/residential/sales'];
|
|
257
|
+
expect(matchPathPattern('/property/residential/sales', patterns)).toBe(
|
|
258
|
+
'/property/residential/sales',
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('matches a searched list path carrying a query string', () => {
|
|
263
|
+
const patterns = ['/property/residential/sales/{id}', '/property/residential/sales'];
|
|
264
|
+
expect(matchPathPattern('/property/residential/sales?SearchTerm=oak', patterns)).toBe(
|
|
265
|
+
'/property/residential/sales',
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('returns null for a path no pattern covers', () => {
|
|
270
|
+
expect(matchPathPattern('/nope/at/all', ['/property/residential/sales'])).toBeNull();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('names every falsy-but-present key and no absent one', () => {
|
|
274
|
+
const rec = {
|
|
275
|
+
serviceCharge: 0,
|
|
276
|
+
groundRent: 0,
|
|
277
|
+
tenure: 'None',
|
|
278
|
+
alarm: false,
|
|
279
|
+
features: [],
|
|
280
|
+
price: 250000,
|
|
281
|
+
};
|
|
282
|
+
const found = falsyPresentKeys(rec);
|
|
283
|
+
expect(found).toEqual(['alarm', 'features', 'groundRent', 'serviceCharge', 'tenure']);
|
|
284
|
+
expect(found).not.toContain('price');
|
|
285
|
+
expect(found).not.toContain('saleBy');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('treats null as absent, not falsy-present', () => {
|
|
289
|
+
// Loop saying nothing is a different fact from Loop saying zero.
|
|
290
|
+
expect(falsyPresentKeys({ serviceCharge: null })).toEqual([]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('unions keys across records', () => {
|
|
294
|
+
expect(wireKeys([{ a: 1 }, { b: 2 }])).toEqual(['a', 'b']);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('normalises a single object, an array, and a non-object payload', () => {
|
|
298
|
+
expect(censusRecords({ a: 1 })).toEqual([{ a: 1 }]);
|
|
299
|
+
expect(censusRecords([{ a: 1 }])).toEqual([{ a: 1 }]);
|
|
300
|
+
expect(censusRecords('nope')).toEqual([]);
|
|
301
|
+
expect(censusRecords(null)).toEqual([]);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 1. Loop adds a new POST/PUT/PATCH/DELETE → no mapping → test fails
|
|
7
7
|
* and names the orphan endpoint.
|
|
8
|
-
* 2. A tool's `server
|
|
8
|
+
* 2. A tool's `lifelineTool(server, "…")` registration is renamed or removed →
|
|
9
9
|
* the name lookup against `index.ts` source fails.
|
|
10
10
|
* 3. A tool's source file is refactored such that the swagger path
|
|
11
11
|
* family no longer appears literally → the path-fragment lookup
|
|
@@ -120,15 +120,15 @@ describe('Loop swagger write coverage', () => {
|
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
// 2. Every mapping's tool is registered in index.ts (catches deregistration).
|
|
123
|
-
// Match by quoted tool name in proximity to a `
|
|
124
|
-
// each registration is `server
|
|
123
|
+
// Match by quoted tool name in proximity to a `lifelineTool(` call —
|
|
124
|
+
// each registration is `lifelineTool(server,\n "<name>", ...)` or single-line.
|
|
125
125
|
// A tool name that appears only in a comment or unrelated string doesn't
|
|
126
126
|
// match because we require it to be the first non-whitespace argument
|
|
127
|
-
// of a
|
|
127
|
+
// of a lifelineTool( invocation within ~32 chars.
|
|
128
128
|
it.each(ENDPOINT_MAP)('$method $path → tool $tool registered in index.ts', ({ tool }) => {
|
|
129
|
-
const registrationRe = new RegExp(`
|
|
129
|
+
const registrationRe = new RegExp(`lifelineTool\\(\\s*server\\s*,\\s*"${tool.replace(/[.*+?^${}()|[\\\]\\\\]/g, '\\\\$&')}"`);
|
|
130
130
|
const isRegistered = registrationRe.test(indexSrc);
|
|
131
|
-
expect(isRegistered, `tool "${tool}" not registered as server
|
|
131
|
+
expect(isRegistered, `tool "${tool}" not registered as lifelineTool(server, "${tool}", ...) in src/index.ts`).toBe(true);
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
// 3. Every mapping's path fragment appears in the named source file
|
|
@@ -149,6 +149,7 @@ lifelineTool(server,
|
|
|
149
149
|
endDate: z.string().optional().describe("End date filter (sellers/landlords)"),
|
|
150
150
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
151
151
|
limit: z.number().int().optional().describe("Max results (default 200)"),
|
|
152
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
152
153
|
},
|
|
153
154
|
toolHandler(async (p) => peopleSearch(p as Parameters<typeof peopleSearch>[0]))
|
|
154
155
|
);
|
|
@@ -160,6 +161,7 @@ lifelineTool(server,
|
|
|
160
161
|
personId: z.number().int().describe("Person ID"),
|
|
161
162
|
role: z.enum(["buyers", "sellers", "renters", "landlords"]).optional().describe("Role for role-specific detail view"),
|
|
162
163
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
164
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
163
165
|
},
|
|
164
166
|
toolHandler(async (p) => peopleDetail(p as Parameters<typeof peopleDetail>[0]))
|
|
165
167
|
);
|
|
@@ -182,6 +184,7 @@ lifelineTool(server,
|
|
|
182
184
|
propertyTypes: z.string().optional().describe("Comma-separated property types"),
|
|
183
185
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
184
186
|
limit: z.number().int().optional().describe("Max results (default 200)"),
|
|
187
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
185
188
|
},
|
|
186
189
|
toolHandler(async (p) => propertySearch(p as Parameters<typeof propertySearch>[0]))
|
|
187
190
|
);
|
|
@@ -194,6 +197,7 @@ lifelineTool(server,
|
|
|
194
197
|
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
195
198
|
previewHash: z.number().int().optional().describe("Preview hash for public preview URL"),
|
|
196
199
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
200
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
197
201
|
},
|
|
198
202
|
toolHandler(async (p) => propertyDetail(p as Parameters<typeof propertyDetail>[0]))
|
|
199
203
|
);
|
|
@@ -206,6 +210,7 @@ lifelineTool(server,
|
|
|
206
210
|
department: z.enum(["sales", "lettings", "both"]).optional().describe("Department (default: both)"),
|
|
207
211
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
208
212
|
limit: z.number().int().optional().describe("Max results (default 200)"),
|
|
213
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
209
214
|
},
|
|
210
215
|
toolHandler(async (p) => propertyListed(p as Parameters<typeof propertyListed>[0]))
|
|
211
216
|
);
|
|
@@ -217,6 +222,7 @@ lifelineTool(server,
|
|
|
217
222
|
channel: z.enum(["rightmove", "onTheMarket", "zoopla", "website"]).describe("Listing channel"),
|
|
218
223
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
219
224
|
limit: z.number().int().optional().describe("Max results (default 200)"),
|
|
225
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
220
226
|
},
|
|
221
227
|
toolHandler(async (p) => propertySold(p as Parameters<typeof propertySold>[0]))
|
|
222
228
|
);
|
|
@@ -285,6 +291,7 @@ lifelineTool(server,
|
|
|
285
291
|
status: z.string().optional().describe("Viewing status filter"),
|
|
286
292
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
287
293
|
limit: z.number().int().optional().describe("Max results (default 200)"),
|
|
294
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
288
295
|
},
|
|
289
296
|
toolHandler(async (p) => viewingSearch(p as Parameters<typeof viewingSearch>[0]))
|
|
290
297
|
);
|
|
@@ -296,6 +303,7 @@ lifelineTool(server,
|
|
|
296
303
|
viewingId: z.number().int().describe("Viewing ID"),
|
|
297
304
|
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
298
305
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
306
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
299
307
|
},
|
|
300
308
|
toolHandler(async (p) => viewingDetail(p as Parameters<typeof viewingDetail>[0]))
|
|
301
309
|
);
|
|
@@ -341,6 +349,7 @@ lifelineTool(server,
|
|
|
341
349
|
teamName: z.string().min(1).describe("Team that owns the viewing"),
|
|
342
350
|
viewingId: z.number().int().describe("Viewing ID"),
|
|
343
351
|
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
352
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
344
353
|
},
|
|
345
354
|
toolHandler(async (p) => feedbackGet(p as Parameters<typeof feedbackGet>[0]))
|
|
346
355
|
);
|
|
@@ -366,6 +375,7 @@ lifelineTool(server,
|
|
|
366
375
|
"Get team details from Loop CRM. Returns team name, address, phone, and email.",
|
|
367
376
|
{
|
|
368
377
|
teamName: z.string().optional().describe("Query a specific team only (omit for all teams)"),
|
|
378
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
369
379
|
},
|
|
370
380
|
toolHandler(async (p) => teamInfo(p as Parameters<typeof teamInfo>[0]))
|
|
371
381
|
);
|
|
@@ -377,6 +387,7 @@ lifelineTool(server,
|
|
|
377
387
|
agentId: z.string().describe("Agent GUID identifier"),
|
|
378
388
|
searchDate: z.string().describe("Date to check availability (YYYY-MM-DD)"),
|
|
379
389
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
390
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
380
391
|
},
|
|
381
392
|
toolHandler(async (p) => teamAvailability(p as Parameters<typeof teamAvailability>[0]))
|
|
382
393
|
);
|
|
@@ -393,6 +404,7 @@ lifelineTool(server,
|
|
|
393
404
|
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
394
405
|
includeTeamProfile: z.boolean().optional().describe("Include the team profile for this match (default: false)"),
|
|
395
406
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
407
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
396
408
|
},
|
|
397
409
|
toolHandler(async (p) => marketingMatchDetail(p as Parameters<typeof marketingMatchDetail>[0]))
|
|
398
410
|
);
|
|
@@ -404,6 +416,7 @@ lifelineTool(server,
|
|
|
404
416
|
propertyIds: z.array(z.number().int()).describe("Array of property IDs to match"),
|
|
405
417
|
department: z.enum(["sales", "lettings"]).describe("sales or lettings"),
|
|
406
418
|
teamName: z.string().optional().describe("Query a specific team only"),
|
|
419
|
+
raw: z.boolean().optional().describe("Return the raw field set received from Loop instead of prose. Use when a decision turns on whether a field was supplied at all: a key present carrying 0 is a zero, a key absent is one Loop did not send."),
|
|
407
420
|
},
|
|
408
421
|
toolHandler(async (p) => marketingMatchBatch(p as Parameters<typeof marketingMatchBatch>[0]))
|
|
409
422
|
);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Per-request read fidelity signals.
|
|
2
|
+
//
|
|
3
|
+
// A truthiness guard in a formatter drops any value that is 0, false, "" or
|
|
4
|
+
// "None", so a caller cannot tell a zero service charge from one nobody
|
|
5
|
+
// entered. `falsyPresentKeys` names exactly that set, which is what makes the
|
|
6
|
+
// loss visible without guessing what a formatter emitted.
|
|
7
|
+
//
|
|
8
|
+
// Guessing was the alternative and all three forms of it are worse. Tracking
|
|
9
|
+
// key access with a proxy gives the wrong answer by construction, because
|
|
10
|
+
// `if (p.serviceCharge)` reads the key and then discards it. Searching the
|
|
11
|
+
// output string for each value is fragile, since 1250000 prints as £1,250,000.
|
|
12
|
+
// Rewriting all twelve formatters around a shared emitter would be exact and
|
|
13
|
+
// is far outside this change.
|
|
14
|
+
|
|
15
|
+
export function normalisePath(path: string): string {
|
|
16
|
+
const q = path.indexOf("?");
|
|
17
|
+
return q === -1 ? path : path.slice(0, q);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function patternToRegExp(pattern: string): RegExp {
|
|
21
|
+
const escaped = pattern
|
|
22
|
+
.split("/")
|
|
23
|
+
.map((seg) =>
|
|
24
|
+
/^\{.+\}$/.test(seg) ? "[^/]+" : seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
25
|
+
)
|
|
26
|
+
.join("/");
|
|
27
|
+
return new RegExp(`^${escaped}$`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a live request path to the swagger pattern that describes it.
|
|
32
|
+
* Literal patterns win over templated ones, so a concrete path never resolves
|
|
33
|
+
* to a templated sibling that also happens to match.
|
|
34
|
+
*/
|
|
35
|
+
export function matchPathPattern(path: string, patterns: string[]): string | null {
|
|
36
|
+
const live = normalisePath(path);
|
|
37
|
+
const exact = patterns.find((p) => p === live);
|
|
38
|
+
if (exact) return exact;
|
|
39
|
+
for (const p of patterns.filter((c) => c.includes("{"))) {
|
|
40
|
+
if (patternToRegExp(p).test(live)) return p;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalise a payload to the records the census walks. A Loop read returns
|
|
47
|
+
* either one object or an array of them; anything else carries no fields to
|
|
48
|
+
* census and yields nothing rather than a misleading zero-key record.
|
|
49
|
+
*/
|
|
50
|
+
export function censusRecords(payload: unknown): Record<string, unknown>[] {
|
|
51
|
+
if (Array.isArray(payload)) {
|
|
52
|
+
return payload.filter(
|
|
53
|
+
(r): r is Record<string, unknown> =>
|
|
54
|
+
r !== null && typeof r === "object" && !Array.isArray(r)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (payload !== null && typeof payload === "object") {
|
|
58
|
+
return [payload as Record<string, unknown>];
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function wireKeys(payload: unknown): string[] {
|
|
64
|
+
const out = new Set<string>();
|
|
65
|
+
for (const r of censusRecords(payload)) {
|
|
66
|
+
for (const k of Object.keys(r)) out.add(k);
|
|
67
|
+
}
|
|
68
|
+
return [...out].sort();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A key is falsy-present when it arrived carrying a value every truthiness
|
|
73
|
+
* guard discards. `null` and `undefined` are NOT included: those are Loop
|
|
74
|
+
* saying nothing, which is a different fact from Loop saying zero, and
|
|
75
|
+
* collapsing the two would rebuild the defect this exists to expose.
|
|
76
|
+
*/
|
|
77
|
+
export function falsyPresentKeys(payload: unknown): string[] {
|
|
78
|
+
const out = new Set<string>();
|
|
79
|
+
for (const r of censusRecords(payload)) {
|
|
80
|
+
for (const [k, v] of Object.entries(r)) {
|
|
81
|
+
if (v === 0 || v === false || v === "" || v === "None") out.add(k);
|
|
82
|
+
else if (Array.isArray(v) && v.length === 0) out.add(k);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [...out].sort();
|
|
86
|
+
}
|