neotoma 0.18.3 → 0.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.d.ts +10 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +152 -0
- package/dist/actions.js.map +1 -1
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +22 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/docs/developer/cli_reference.md +2 -1
- package/dist/docs/developer/fleet_onboarding.md +23 -0
- package/dist/docs/subsystems/conflict_resolution.md +72 -30
- package/dist/docs/subsystems/inspector_embed_cross_origin.md +163 -0
- package/dist/docs/testing/automated_test_catalog.md +16 -8
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +80 -0
- package/dist/server.js.map +1 -1
- package/dist/services/embed_cross_origin.d.ts +95 -0
- package/dist/services/embed_cross_origin.d.ts.map +1 -0
- package/dist/services/embed_cross_origin.js +174 -0
- package/dist/services/embed_cross_origin.js.map +1 -0
- package/dist/services/null_cleared_field_warning.d.ts +71 -0
- package/dist/services/null_cleared_field_warning.d.ts.map +1 -0
- package/dist/services/null_cleared_field_warning.js +77 -0
- package/dist/services/null_cleared_field_warning.js.map +1 -0
- package/dist/services/source_priority_warning.d.ts +57 -0
- package/dist/services/source_priority_warning.d.ts.map +1 -1
- package/dist/services/source_priority_warning.js +84 -0
- package/dist/services/source_priority_warning.js.map +1 -1
- package/dist/shared/action_schemas.d.ts +6 -0
- package/dist/shared/action_schemas.d.ts.map +1 -1
- package/dist/shared/action_schemas.js +6 -0
- package/dist/shared/action_schemas.js.map +1 -1
- package/package.json +2 -1
- package/skills/end/SKILL.md +28 -2
- package/skills/status/SKILL.md +27 -1
package/dist/actions.d.ts
CHANGED
|
@@ -97,6 +97,16 @@ type GuestPrincipal = {
|
|
|
97
97
|
type RoutePrincipal = UserPrincipal | GuestPrincipal;
|
|
98
98
|
/** Exported for unit tests: routes where guest (AAuth / guest token) may be stamped before handlers run. */
|
|
99
99
|
export declare function routeAcceptsGuestPrincipal(req: Pick<express.Request, "method" | "path">): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Exported for unit tests: the two issue-write routes that extend local-loopback
|
|
102
|
+
* trust to unauthenticated requests. A local request to `/issues/submit` or
|
|
103
|
+
* `/issues/add_message` with no `Authorization: Bearer` header falls through to
|
|
104
|
+
* the local dev user — the same offline-developer trust the entity-read routes
|
|
105
|
+
* already grant (see `resolveGuestUserId`). This is the ONLY place that trust is
|
|
106
|
+
* widened; every other route still requires a Bearer token, and remote requests
|
|
107
|
+
* (non-loopback / untrusted XFF) get AUTH_REQUIRED on these routes too.
|
|
108
|
+
*/
|
|
109
|
+
export declare function routeAllowsLocalIssueWriteFallback(req: Pick<express.Request, "method" | "path">): boolean;
|
|
100
110
|
/** Exported for unit tests: guest-capable routes that mutate state and should consume a guest write-rate budget. */
|
|
101
111
|
export declare function routeConsumesGuestWriteBudget(req: Pick<express.Request, "method" | "path">): boolean;
|
|
102
112
|
/** Exported for unit tests: guest-rate-limit key precedence is thumbprint -> guest token -> IP. */
|
package/dist/actions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAsC9B,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,6BAA6B,CAAC;AAarC,OAAO,EAGL,KAAK,aAAa,EAGnB,MAAM,4BAA4B,CAAC;AA4BpC,OAAO,EAQL,KAAK,sBAAsB,EAC5B,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAsC9B,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,6BAA6B,CAAC;AAarC,OAAO,EAGL,KAAK,aAAa,EAGnB,MAAM,4BAA4B,CAAC;AA4BpC,OAAO,EAQL,KAAK,sBAAsB,EAC5B,MAAM,4BAA4B,CAAC;AAuDpC,OAAO,EAiCL,KAAK,wBAAwB,EAE9B,MAAM,4BAA4B,CAAC;AA8CpC,6FAA6F;AAC7F,wBAAgB,qBAAqB,IAAI,sBAAsB,GAAG,IAAI,CAErE;AAED,eAAO,MAAM,GAAG,6CAAY,CAAC;AAqwB7B,kFAAkF;AAClF,eAAO,MAAM,4BAA4B,QAAS,CAAC;AAEnD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMtE;AAoBD,uFAAuF;AACvF,eAAO,MAAM,wBAAwB,OAAQ,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMlE;AAID;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,OAAO,CAAC,OAAO,EACpB,GAAG,EAAE,OAAO,CAAC,QAAQ,EACrB,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAA;CAAE,GAC7F,IAAI,CA4IN;AAuBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CA+B1F;AAmCD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CA2B5D;AA4PD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CA2BhD;AA47CD,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;IACvB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,cAAc,GAAG,aAAa,GAAG,cAAc,CAAC;AA2CrD,4GAA4G;AAC5G,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,GAAG,MAAM,CAAC,GAAG,OAAO,CAqBjG;AAED;;;;;;;;GAQG;AACH,wBAAgB,kCAAkC,CAChD,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,GAAG,MAAM,CAAC,GAC5C,OAAO,CAST;AAED,oHAAoH;AACpH,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,GAAG,MAAM,CAAC,GAC5C,OAAO,CAWT;AAED,mGAAmG;AACnG,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,MAAM,CAanE;AAgHD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,CAAC,OAAO,EACpB,SAAS,EAAE,cAAc,GACxB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoCxB;AA69FD,KAAK,oBAAoB,GAAG;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CAAC;AAuDF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,4BAA4B,EAAE,iBAAiB,CAAC;IAC3E,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACvC,cAAc,CAAC,EAAE,wBAAwB,CAAC;IAC1C,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;;;;;;;;;;;;;;qBAu5BgB,MAAM;eACZ,MAAM;2BACM,MAAM;;;;;;cAlBnB,MAAM;iBACH,MAAM;2BACI,MAAM;qBACZ,MAAM;mBACR,MAAM;;;cA/BX,MAAM;gBACJ,MAAM;2BACK,MAAM;qBACZ,MAAM;mBACR,MAAM;wBACD,MAAM;uBACP,MAAM;;;;;mBAhQV,MAAM;qBACJ,MAAM;wBACH,MAAM,GAAG,IAAI;2BACV,MAAM;;wBAET,MAAM;uBACP,MAAM,EAAE;wBACP,MAAM;uBACP,MAAM;;kBAxYX,MAAM;oBACJ,MAAM;yBACD,MAAM;4BACH,MAAM;2BACP,MAAM;;;kBAQf,MAAM;yBACC,MAAM;iCACE,MAAM;sCACD,MAAM;;+BA4Xb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;;;2BAwKlC,MAAM;0BACP,MAAM;0BACN,MAAM;;;;;;;;GA8b3B;AAigHD,wBAAsB,eAAe;;;eAmOpC"}
|
package/dist/actions.js
CHANGED
|
@@ -47,6 +47,7 @@ import { getSandboxPack } from "./services/sandbox/pack_registry.js";
|
|
|
47
47
|
import { buildEndpointsMap, buildLandingContext, buildRootLandingJson, buildRootLandingMarkdown, buildRobotsTxt, readGitSha, readNeotomaConfigEnvironment, readPackageVersion, resolveLandingMode, wantsHtml as acceptWantsHtml, wantsMarkdown as acceptWantsMarkdown, } from "./services/root_landing/index.js";
|
|
48
48
|
import { mountDocsRoutes } from "./services/docs/index.js";
|
|
49
49
|
import { installInspectorLegacyRedirect, installInspectorRootStaticAssets, installInspectorSpaFallback, installInspectorSpaShellEarly, resolveBundledInspectorDir, } from "./services/inspector_mount.js";
|
|
50
|
+
import { EMBED_ALLOWED_ORIGINS_ENV, applyFrameAncestorsToCsp, buildFrameAncestorsDirective, embedCorsHeaders, isEmbedReadEndpoint, isEmbedShellPath, isOriginAllowed, parseAllowedEmbedOrigins, } from "./services/embed_cross_origin.js";
|
|
50
51
|
import { getSandboxTermsResponse } from "./services/sandbox/terms.js";
|
|
51
52
|
import { resolveSandboxReportTransport } from "./services/sandbox/transport.js";
|
|
52
53
|
import { getSqliteDb } from "./repositories/sqlite/sqlite_client.js";
|
|
@@ -113,6 +114,71 @@ app.use(helmet({
|
|
|
113
114
|
},
|
|
114
115
|
},
|
|
115
116
|
}));
|
|
117
|
+
// ── Official cross-origin Inspector-embed support (opt-in, secure by default) ──
|
|
118
|
+
//
|
|
119
|
+
// When NEOTOMA_EMBED_ALLOWED_ORIGINS lists one or more host origins, an
|
|
120
|
+
// allowlisted host may iframe `/embed/graph` directly and its browser may
|
|
121
|
+
// `fetch()` the two graph read endpoints cross-origin — WITHOUT running a
|
|
122
|
+
// same-origin reverse proxy. Two response behaviors change, ONLY for
|
|
123
|
+
// allowlisted traffic:
|
|
124
|
+
//
|
|
125
|
+
// 1. Embed shell paths (`/embed/*`) get `Content-Security-Policy:
|
|
126
|
+
// frame-ancestors 'self' <origins>` and drop `X-Frame-Options`, so the
|
|
127
|
+
// allowlisted host can frame them. Non-embed paths are untouched.
|
|
128
|
+
// 2. The two blessed read endpoints (`/entities/query`,
|
|
129
|
+
// `/retrieve_graph_neighborhood`) get scoped CORS (exact-origin echo,
|
|
130
|
+
// never `*`, no credentials) + preflight handling — and ONLY those two.
|
|
131
|
+
// Write endpoints are never CORS-enabled here, so a browser on an
|
|
132
|
+
// allowlisted origin can never reach `/store`,`/correct`,… cross-origin.
|
|
133
|
+
//
|
|
134
|
+
// SECURE BY DEFAULT: with no allowlist configured, this middleware is inert and
|
|
135
|
+
// every response is byte-identical to the locked-down `'self'`/`SAMEORIGIN`
|
|
136
|
+
// posture. Registered AFTER Helmet (so it can override the `X-Frame-Options`
|
|
137
|
+
// Helmet set and rewrite Helmet's CSP), BEFORE the global `cors()` (so it owns
|
|
138
|
+
// the embed preflight and its response is not pre-empted), and before the auth
|
|
139
|
+
// stack (so a CORS preflight OPTIONS is answered without a bearer, as browsers
|
|
140
|
+
// require).
|
|
141
|
+
const embedAllowedOrigins = parseAllowedEmbedOrigins(process.env[EMBED_ALLOWED_ORIGINS_ENV]);
|
|
142
|
+
if (embedAllowedOrigins.length > 0) {
|
|
143
|
+
logger.info(`[EmbedCORS] Cross-origin Inspector embed enabled for: ${embedAllowedOrigins.join(", ")}`);
|
|
144
|
+
}
|
|
145
|
+
app.use((req, res, next) => {
|
|
146
|
+
if (embedAllowedOrigins.length === 0)
|
|
147
|
+
return next();
|
|
148
|
+
const requestOrigin = req.headers.origin;
|
|
149
|
+
const reqPath = req.path;
|
|
150
|
+
// (1) Relax framing on the embed shell so an allowlisted host can iframe it.
|
|
151
|
+
// Applied on embed paths (framing is enforced by the browser against the CSP
|
|
152
|
+
// `frame-ancestors` list, which is itself restricted to the configured
|
|
153
|
+
// origins — so emitting it grants nothing beyond the allowlist). Rewrite the
|
|
154
|
+
// existing Helmet CSP if present, else set a standalone `frame-ancestors`
|
|
155
|
+
// policy, and remove `X-Frame-Options` (which has no allowlist form and would
|
|
156
|
+
// otherwise still block the frame).
|
|
157
|
+
if (isEmbedShellPath(reqPath)) {
|
|
158
|
+
const existingCsp = res.getHeader("Content-Security-Policy");
|
|
159
|
+
if (typeof existingCsp === "string") {
|
|
160
|
+
res.setHeader("Content-Security-Policy", applyFrameAncestorsToCsp(existingCsp, embedAllowedOrigins));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const directive = buildFrameAncestorsDirective(embedAllowedOrigins);
|
|
164
|
+
if (directive)
|
|
165
|
+
res.setHeader("Content-Security-Policy", directive);
|
|
166
|
+
}
|
|
167
|
+
res.removeHeader("X-Frame-Options");
|
|
168
|
+
}
|
|
169
|
+
// (2) Scoped CORS on the two blessed read endpoints, only for an allowlisted
|
|
170
|
+
// Origin. Preflight (OPTIONS) is answered here directly with 204.
|
|
171
|
+
if (isEmbedReadEndpoint(reqPath) && isOriginAllowed(requestOrigin, embedAllowedOrigins)) {
|
|
172
|
+
const canonical = new URL(requestOrigin).origin;
|
|
173
|
+
for (const [name, value] of Object.entries(embedCorsHeaders(canonical))) {
|
|
174
|
+
res.setHeader(name, value);
|
|
175
|
+
}
|
|
176
|
+
if (req.method === "OPTIONS") {
|
|
177
|
+
return res.status(204).end();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return next();
|
|
181
|
+
});
|
|
116
182
|
// Configure CORS to allow frontend origin
|
|
117
183
|
const corsOptions = {
|
|
118
184
|
origin: process.env.NEOTOMA_FRONTEND_URL || process.env.FRONTEND_URL || "http://localhost:5195",
|
|
@@ -2611,6 +2677,23 @@ export function routeAcceptsGuestPrincipal(req) {
|
|
|
2611
2677
|
}
|
|
2612
2678
|
return false;
|
|
2613
2679
|
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Exported for unit tests: the two issue-write routes that extend local-loopback
|
|
2682
|
+
* trust to unauthenticated requests. A local request to `/issues/submit` or
|
|
2683
|
+
* `/issues/add_message` with no `Authorization: Bearer` header falls through to
|
|
2684
|
+
* the local dev user — the same offline-developer trust the entity-read routes
|
|
2685
|
+
* already grant (see `resolveGuestUserId`). This is the ONLY place that trust is
|
|
2686
|
+
* widened; every other route still requires a Bearer token, and remote requests
|
|
2687
|
+
* (non-loopback / untrusted XFF) get AUTH_REQUIRED on these routes too.
|
|
2688
|
+
*/
|
|
2689
|
+
export function routeAllowsLocalIssueWriteFallback(req) {
|
|
2690
|
+
const path = req.path;
|
|
2691
|
+
return (req.method === "POST" &&
|
|
2692
|
+
(path === "/issues/submit" ||
|
|
2693
|
+
path === "/api/issues/submit" ||
|
|
2694
|
+
path === "/issues/add_message" ||
|
|
2695
|
+
path === "/api/issues/add_message"));
|
|
2696
|
+
}
|
|
2614
2697
|
/** Exported for unit tests: guest-capable routes that mutate state and should consume a guest write-rate budget. */
|
|
2615
2698
|
export function routeConsumesGuestWriteBudget(req) {
|
|
2616
2699
|
const path = req.path;
|
|
@@ -2704,6 +2787,24 @@ async function resolveRoutePrincipal(req, accept) {
|
|
|
2704
2787
|
}
|
|
2705
2788
|
}
|
|
2706
2789
|
if (accept.includes("user")) {
|
|
2790
|
+
// Local-loopback trust for the two issue-write routes: a local request with
|
|
2791
|
+
// no Bearer token resolves to the local dev user instead of failing
|
|
2792
|
+
// AUTH_REQUIRED. This mirrors the existing local fallback in
|
|
2793
|
+
// `resolveGuestUserId` and keeps `neotoma issues create` working offline
|
|
2794
|
+
// (gh-CLI auth alone does not configure a Neotoma Bearer token). Scoped as
|
|
2795
|
+
// narrowly as possible: ONLY these routes, ONLY local requests, ONLY when
|
|
2796
|
+
// no Bearer header is present — remote/Bearer requests fall through to the
|
|
2797
|
+
// normal `getAuthenticatedUserId` gate below.
|
|
2798
|
+
const headerAuth = (req.headers.authorization || req.headers.Authorization || "");
|
|
2799
|
+
if (routeAllowsLocalIssueWriteFallback(req) &&
|
|
2800
|
+
isLocalRequest(req) &&
|
|
2801
|
+
!headerAuth.startsWith("Bearer ")) {
|
|
2802
|
+
const userId = _localSandboxActive ? ensureLocalSandboxUser().id : ensureLocalDevUser().id;
|
|
2803
|
+
// Stamp the principal so a later `getAuthenticatedUserId` in the handler
|
|
2804
|
+
// resolves the same local user without re-tripping the missing-Bearer gate.
|
|
2805
|
+
stampUserPrincipal(req, userId);
|
|
2806
|
+
return { kind: "user", userId };
|
|
2807
|
+
}
|
|
2707
2808
|
const userId = await getAuthenticatedUserId(req, (req.body?.user_id ??
|
|
2708
2809
|
req.query.user_id));
|
|
2709
2810
|
return { kind: "user", userId };
|
|
@@ -5704,6 +5805,12 @@ export async function storeStructuredForApi(params) {
|
|
|
5704
5805
|
}
|
|
5705
5806
|
}
|
|
5706
5807
|
const createdEntities = [];
|
|
5808
|
+
// Issue #1839: capture each entity's prior and post-reduction snapshot so the
|
|
5809
|
+
// store_warnings loop below can detect a NULL_CLEARED_FIELD event (an incoming
|
|
5810
|
+
// null clearing a prior non-null value under highest_priority). Keyed by
|
|
5811
|
+
// entity_id.
|
|
5812
|
+
const priorSnapshotByEntityId = new Map();
|
|
5813
|
+
const newSnapshotByEntityId = new Map();
|
|
5707
5814
|
for (const r of resolved) {
|
|
5708
5815
|
let observation_id = null;
|
|
5709
5816
|
let snapshotAfter = null;
|
|
@@ -5715,6 +5822,7 @@ export async function storeStructuredForApi(params) {
|
|
|
5715
5822
|
.eq("user_id", userId)
|
|
5716
5823
|
.maybeSingle();
|
|
5717
5824
|
const priorSnapshot = priorSnapRow?.snapshot ?? {};
|
|
5825
|
+
priorSnapshotByEntityId.set(r.entity_id, priorSnapshot);
|
|
5718
5826
|
const obsData = await createObservation({
|
|
5719
5827
|
entity_id: r.entity_id,
|
|
5720
5828
|
entity_type: r.entity_type,
|
|
@@ -5737,6 +5845,8 @@ export async function storeStructuredForApi(params) {
|
|
|
5737
5845
|
const snap = await recomputeSnapshot(r.entity_id, userId);
|
|
5738
5846
|
snapshotAfter =
|
|
5739
5847
|
snap?.snapshot ?? null;
|
|
5848
|
+
if (snapshotAfter)
|
|
5849
|
+
newSnapshotByEntityId.set(r.entity_id, snapshotAfter);
|
|
5740
5850
|
}
|
|
5741
5851
|
catch (snapshotErr) {
|
|
5742
5852
|
logger.warn(`Snapshot recompute failed for ${r.entity_id}: ${snapshotErr}`);
|
|
@@ -6076,6 +6186,48 @@ export async function storeStructuredForApi(params) {
|
|
|
6076
6186
|
});
|
|
6077
6187
|
if (spWarn)
|
|
6078
6188
|
schemaStoreWarnings.push(spWarn);
|
|
6189
|
+
// Issue #1838: SOURCE_PRIORITY_ESCALATION — mirror-image advisory. A
|
|
6190
|
+
// write at the DEFAULT source_priority (100) into a `highest_priority`
|
|
6191
|
+
// field silently OUTRANKS any prior observation written with an
|
|
6192
|
+
// explicit lower priority. Non-blocking; reuses the same import.
|
|
6193
|
+
const { buildSourcePriorityEscalationWarning } = await import("./services/source_priority_warning.js");
|
|
6194
|
+
const spEsc = buildSourcePriorityEscalationWarning({
|
|
6195
|
+
sourcePriority,
|
|
6196
|
+
writtenFields: r.fields,
|
|
6197
|
+
mergePolicies: schemaEntry?.reducer_config?.merge_policies,
|
|
6198
|
+
observationIndex: r.observation_index,
|
|
6199
|
+
entityType: r.entity_type,
|
|
6200
|
+
entityId: r.entity_id,
|
|
6201
|
+
});
|
|
6202
|
+
if (spEsc)
|
|
6203
|
+
schemaStoreWarnings.push(spEsc);
|
|
6204
|
+
}
|
|
6205
|
+
// Issue #1839: NULL_CLEARED_FIELD — non-blocking advisory warning when an
|
|
6206
|
+
// incoming null clears a prior non-null value under a `highest_priority`
|
|
6207
|
+
// field. The clear is by design (null is an explicit tombstone), but it
|
|
6208
|
+
// is silent today; this surfaces the data-loss vector. Warn-only: no
|
|
6209
|
+
// change to snapshot computation or clearing semantics.
|
|
6210
|
+
{
|
|
6211
|
+
const mergePolicies = schemaEntry?.reducer_config?.merge_policies;
|
|
6212
|
+
if (mergePolicies) {
|
|
6213
|
+
const priorSnapshot = priorSnapshotByEntityId.get(r.entity_id) ?? {};
|
|
6214
|
+
const newSnapshot = newSnapshotByEntityId.get(r.entity_id) ?? {};
|
|
6215
|
+
const { buildNullClearedFieldWarning } = await import("./services/null_cleared_field_warning.js");
|
|
6216
|
+
for (const field of Object.keys(r.fields)) {
|
|
6217
|
+
const nullWarn = buildNullClearedFieldWarning({
|
|
6218
|
+
field,
|
|
6219
|
+
strategy: mergePolicies[field]?.strategy,
|
|
6220
|
+
prior_value: priorSnapshot[field],
|
|
6221
|
+
incoming_value: r.fields[field],
|
|
6222
|
+
new_value: newSnapshot[field],
|
|
6223
|
+
observation_index: r.observation_index,
|
|
6224
|
+
entity_type: r.entity_type,
|
|
6225
|
+
entity_id: r.entity_id,
|
|
6226
|
+
});
|
|
6227
|
+
if (nullWarn)
|
|
6228
|
+
schemaStoreWarnings.push(nullWarn);
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6079
6231
|
}
|
|
6080
6232
|
}
|
|
6081
6233
|
}
|