@ryanstark24/sfgraph-core 1.2.0 → 1.4.0
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/analyze/__tests__/audit-graph.test.js +79 -0
- package/dist/analyze/__tests__/audit-graph.test.js.map +1 -1
- package/dist/analyze/__tests__/edge-filters.test.d.ts +2 -0
- package/dist/analyze/__tests__/edge-filters.test.d.ts.map +1 -0
- package/dist/analyze/__tests__/edge-filters.test.js +66 -0
- package/dist/analyze/__tests__/edge-filters.test.js.map +1 -0
- package/dist/analyze/__tests__/findings.test.js +19 -9
- package/dist/analyze/__tests__/findings.test.js.map +1 -1
- package/dist/analyze/__tests__/governor-risks-detection.test.js +90 -0
- package/dist/analyze/__tests__/governor-risks-detection.test.js.map +1 -1
- package/dist/analyze/__tests__/security-fls-filter.test.d.ts +2 -0
- package/dist/analyze/__tests__/security-fls-filter.test.d.ts.map +1 -0
- package/dist/analyze/__tests__/security-fls-filter.test.js +23 -0
- package/dist/analyze/__tests__/security-fls-filter.test.js.map +1 -0
- package/dist/analyze/audit-graph.d.ts +26 -2
- package/dist/analyze/audit-graph.d.ts.map +1 -1
- package/dist/analyze/audit-graph.js +52 -5
- package/dist/analyze/audit-graph.js.map +1 -1
- package/dist/analyze/dependencies.d.ts +2 -2
- package/dist/analyze/dependencies.d.ts.map +1 -1
- package/dist/analyze/dependencies.js +9 -3
- package/dist/analyze/dependencies.js.map +1 -1
- package/dist/analyze/dependents.d.ts +6 -1
- package/dist/analyze/dependents.d.ts.map +1 -1
- package/dist/analyze/dependents.js +12 -2
- package/dist/analyze/dependents.js.map +1 -1
- package/dist/analyze/edge-filters.d.ts +37 -0
- package/dist/analyze/edge-filters.d.ts.map +1 -0
- package/dist/analyze/edge-filters.js +51 -0
- package/dist/analyze/edge-filters.js.map +1 -0
- package/dist/analyze/find-nodes.d.ts.map +1 -1
- package/dist/analyze/find-nodes.js.map +1 -1
- package/dist/analyze/findings.d.ts +1 -1
- package/dist/analyze/findings.d.ts.map +1 -1
- package/dist/analyze/findings.js +1 -3
- package/dist/analyze/findings.js.map +1 -1
- package/dist/analyze/populate.d.ts.map +1 -1
- package/dist/analyze/populate.js +55 -10
- package/dist/analyze/populate.js.map +1 -1
- package/dist/analyze/security.d.ts +15 -0
- package/dist/analyze/security.d.ts.map +1 -1
- package/dist/analyze/security.js +59 -3
- package/dist/analyze/security.js.map +1 -1
- package/dist/analyze/wip-impact.d.ts.map +1 -1
- package/dist/analyze/wip-impact.js +12 -2
- package/dist/analyze/wip-impact.js.map +1 -1
- package/dist/domain/metadata-category.d.ts +1 -0
- package/dist/domain/metadata-category.d.ts.map +1 -1
- package/dist/domain/metadata-category.js +1 -0
- package/dist/domain/metadata-category.js.map +1 -1
- package/dist/embedding/embed.d.ts.map +1 -1
- package/dist/embedding/embed.js +14 -1
- package/dist/embedding/embed.js.map +1 -1
- package/dist/extractors/interfaces/metadata-source.d.ts +11 -0
- package/dist/extractors/interfaces/metadata-source.d.ts.map +1 -1
- package/dist/extractors/live-org/__tests__/bulk-retrieve.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/custom-settings.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/custom-settings.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/custom-settings.test.js +79 -0
- package/dist/extractors/live-org/__tests__/custom-settings.test.js.map +1 -0
- package/dist/extractors/live-org/__tests__/extractor-enrichment.test.js +70 -8
- package/dist/extractors/live-org/__tests__/extractor-enrichment.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/mcd-baseline.test.js +9 -6
- package/dist/extractors/live-org/__tests__/mcd-baseline.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/metadata-read-batch.test.js +1 -1
- package/dist/extractors/live-org/__tests__/metadata-read-batch.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.js +1 -1
- package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/pools.test.js +4 -1
- package/dist/extractors/live-org/__tests__/pools.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/rate-limit.test.js +1 -6
- package/dist/extractors/live-org/__tests__/rate-limit.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/soql-rebatch.test.js.map +1 -1
- package/dist/extractors/live-org/__tests__/stop-waiting.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/stop-waiting.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/stop-waiting.test.js +243 -0
- package/dist/extractors/live-org/__tests__/stop-waiting.test.js.map +1 -0
- package/dist/extractors/live-org/__tests__/watchdog-budgets.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/watchdog-budgets.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/watchdog-budgets.test.js +40 -0
- package/dist/extractors/live-org/__tests__/watchdog-budgets.test.js.map +1 -0
- package/dist/extractors/live-org/__tests__/wedge-isolation.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/wedge-isolation.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/wedge-isolation.test.js +270 -0
- package/dist/extractors/live-org/__tests__/wedge-isolation.test.js.map +1 -0
- package/dist/extractors/live-org/bulk-retrieve.d.ts +169 -0
- package/dist/extractors/live-org/bulk-retrieve.d.ts.map +1 -1
- package/dist/extractors/live-org/bulk-retrieve.js +499 -70
- package/dist/extractors/live-org/bulk-retrieve.js.map +1 -1
- package/dist/extractors/live-org/capabilities.d.ts.map +1 -1
- package/dist/extractors/live-org/capabilities.js.map +1 -1
- package/dist/extractors/live-org/extractors/__tests__/apex.test.d.ts +2 -0
- package/dist/extractors/live-org/extractors/__tests__/apex.test.d.ts.map +1 -0
- package/dist/extractors/live-org/extractors/__tests__/apex.test.js +73 -0
- package/dist/extractors/live-org/extractors/__tests__/apex.test.js.map +1 -0
- package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.d.ts +2 -0
- package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.d.ts.map +1 -0
- package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.js +183 -0
- package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.js.map +1 -0
- package/dist/extractors/live-org/extractors/apex.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/apex.js +76 -51
- package/dist/extractors/live-org/extractors/apex.js.map +1 -1
- package/dist/extractors/live-org/extractors/custom-settings.d.ts +3 -0
- package/dist/extractors/live-org/extractors/custom-settings.d.ts.map +1 -0
- package/dist/extractors/live-org/extractors/custom-settings.js +66 -0
- package/dist/extractors/live-org/extractors/custom-settings.js.map +1 -0
- package/dist/extractors/live-org/extractors/flow.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/flow.js.map +1 -1
- package/dist/extractors/live-org/extractors/generic-metadata.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/generic-metadata.js.map +1 -1
- package/dist/extractors/live-org/extractors/generic-sobject.js.map +1 -1
- package/dist/extractors/live-org/extractors/lwc.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/lwc.js +81 -41
- package/dist/extractors/live-org/extractors/lwc.js.map +1 -1
- package/dist/extractors/live-org/extractors/mcd-baseline.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/mcd-baseline.js +2 -4
- package/dist/extractors/live-org/extractors/mcd-baseline.js.map +1 -1
- package/dist/extractors/live-org/extractors/object.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/object.js.map +1 -1
- package/dist/extractors/live-org/extractors/omnistudio-retrieve.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/omnistudio-retrieve.js.map +1 -1
- package/dist/extractors/live-org/extractors/omnistudio.d.ts +8 -1
- package/dist/extractors/live-org/extractors/omnistudio.d.ts.map +1 -1
- package/dist/extractors/live-org/extractors/omnistudio.js +59 -6
- package/dist/extractors/live-org/extractors/omnistudio.js.map +1 -1
- package/dist/extractors/live-org/rate-limit.d.ts +41 -15
- package/dist/extractors/live-org/rate-limit.d.ts.map +1 -1
- package/dist/extractors/live-org/rate-limit.js +185 -33
- package/dist/extractors/live-org/rate-limit.js.map +1 -1
- package/dist/extractors/live-org/vlocity/runner.d.ts.map +1 -1
- package/dist/extractors/live-org/vlocity/runner.js +30 -6
- package/dist/extractors/live-org/vlocity/runner.js.map +1 -1
- package/dist/ingest/__tests__/config-values.test.d.ts +2 -0
- package/dist/ingest/__tests__/config-values.test.d.ts.map +1 -0
- package/dist/ingest/__tests__/config-values.test.js +65 -0
- package/dist/ingest/__tests__/config-values.test.js.map +1 -0
- package/dist/ingest/__tests__/full-sync-deletion.test.js +11 -4
- package/dist/ingest/__tests__/full-sync-deletion.test.js.map +1 -1
- package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.d.ts +10 -0
- package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.d.ts.map +1 -0
- package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.js +251 -0
- package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.js.map +1 -0
- package/dist/ingest/__tests__/live-ingest.test.js.map +1 -1
- package/dist/ingest/__tests__/parser-dispatch.test.d.ts +2 -0
- package/dist/ingest/__tests__/parser-dispatch.test.d.ts.map +1 -0
- package/dist/ingest/__tests__/parser-dispatch.test.js +55 -0
- package/dist/ingest/__tests__/parser-dispatch.test.js.map +1 -0
- package/dist/ingest/__tests__/vlocity-dispatch-integration.test.d.ts +2 -0
- package/dist/ingest/__tests__/vlocity-dispatch-integration.test.d.ts.map +1 -0
- package/dist/ingest/__tests__/vlocity-dispatch-integration.test.js +134 -0
- package/dist/ingest/__tests__/vlocity-dispatch-integration.test.js.map +1 -0
- package/dist/ingest/detect-deletions-guard.d.ts +45 -0
- package/dist/ingest/detect-deletions-guard.d.ts.map +1 -0
- package/dist/ingest/detect-deletions-guard.js +63 -0
- package/dist/ingest/detect-deletions-guard.js.map +1 -0
- package/dist/ingest/live-ingest.d.ts +16 -0
- package/dist/ingest/live-ingest.d.ts.map +1 -1
- package/dist/ingest/live-ingest.js +776 -549
- package/dist/ingest/live-ingest.js.map +1 -1
- package/dist/parsers/__tests__/datatransform-structured.test.d.ts +2 -0
- package/dist/parsers/__tests__/datatransform-structured.test.d.ts.map +1 -0
- package/dist/parsers/__tests__/datatransform-structured.test.js +147 -0
- package/dist/parsers/__tests__/datatransform-structured.test.js.map +1 -0
- package/dist/parsers/__tests__/edge-provenance.test.js +4 -1
- package/dist/parsers/__tests__/edge-provenance.test.js.map +1 -1
- package/dist/parsers/__tests__/lwc-bindings.test.js +6 -3
- package/dist/parsers/__tests__/lwc-bindings.test.js.map +1 -1
- package/dist/parsers/__tests__/object-inline-fields.test.js +2 -2
- package/dist/parsers/__tests__/object-inline-fields.test.js.map +1 -1
- package/dist/parsers/__tests__/omnistudio.golden.test.js +5 -6
- package/dist/parsers/__tests__/omnistudio.golden.test.js.map +1 -1
- package/dist/parsers/__tests__/vlocity.golden.test.js +9 -13
- package/dist/parsers/__tests__/vlocity.golden.test.js.map +1 -1
- package/dist/parsers/apex/__tests__/ast-extractor.test.js +64 -4
- package/dist/parsers/apex/__tests__/ast-extractor.test.js.map +1 -1
- package/dist/parsers/apex/__tests__/empty-body.test.d.ts +2 -0
- package/dist/parsers/apex/__tests__/empty-body.test.d.ts.map +1 -0
- package/dist/parsers/apex/__tests__/empty-body.test.js +40 -0
- package/dist/parsers/apex/__tests__/empty-body.test.js.map +1 -0
- package/dist/parsers/apex/__tests__/is-test-attribute.test.js +4 -2
- package/dist/parsers/apex/__tests__/is-test-attribute.test.js.map +1 -1
- package/dist/parsers/apex/__tests__/regex-arg-count.test.js +1 -1
- package/dist/parsers/apex/__tests__/regex-arg-count.test.js.map +1 -1
- package/dist/parsers/apex/__tests__/regex-inloop.test.d.ts +2 -0
- package/dist/parsers/apex/__tests__/regex-inloop.test.d.ts.map +1 -0
- package/dist/parsers/apex/__tests__/regex-inloop.test.js +70 -0
- package/dist/parsers/apex/__tests__/regex-inloop.test.js.map +1 -0
- package/dist/parsers/apex/arity-resolver.d.ts.map +1 -1
- package/dist/parsers/apex/arity-resolver.js.map +1 -1
- package/dist/parsers/apex/ast-extractor.d.ts.map +1 -1
- package/dist/parsers/apex/ast-extractor.js +33 -1
- package/dist/parsers/apex/ast-extractor.js.map +1 -1
- package/dist/parsers/apex/class.d.ts.map +1 -1
- package/dist/parsers/apex/class.js +62 -3
- package/dist/parsers/apex/class.js.map +1 -1
- package/dist/parsers/apex/trigger.d.ts.map +1 -1
- package/dist/parsers/apex/trigger.js +10 -0
- package/dist/parsers/apex/trigger.js.map +1 -1
- package/dist/parsers/flow/__tests__/invocable-resolver.test.js.map +1 -1
- package/dist/parsers/flow/invocable-resolver.d.ts.map +1 -1
- package/dist/parsers/flow/invocable-resolver.js.map +1 -1
- package/dist/parsers/generic/reflection-walker.d.ts.map +1 -1
- package/dist/parsers/generic/reflection-walker.js +1 -3
- package/dist/parsers/generic/reflection-walker.js.map +1 -1
- package/dist/parsers/lwc/html-visitor.d.ts.map +1 -1
- package/dist/parsers/lwc/html-visitor.js +3 -2
- package/dist/parsers/lwc/html-visitor.js.map +1 -1
- package/dist/parsers/lwc/js-visitor.d.ts.map +1 -1
- package/dist/parsers/lwc/js-visitor.js +6 -2
- package/dist/parsers/lwc/js-visitor.js.map +1 -1
- package/dist/parsers/object/object.d.ts.map +1 -1
- package/dist/parsers/object/object.js +1 -5
- package/dist/parsers/object/object.js.map +1 -1
- package/dist/parsers/omnistudio/data-transform.d.ts.map +1 -1
- package/dist/parsers/omnistudio/data-transform.js +39 -6
- package/dist/parsers/omnistudio/data-transform.js.map +1 -1
- package/dist/parsers/omnistudio/integration-procedure.d.ts.map +1 -1
- package/dist/parsers/omnistudio/integration-procedure.js +3 -2
- package/dist/parsers/omnistudio/integration-procedure.js.map +1 -1
- package/dist/parsers/omnistudio/overlap-detector.d.ts.map +1 -1
- package/dist/parsers/omnistudio/overlap-detector.js +2 -9
- package/dist/parsers/omnistudio/overlap-detector.js.map +1 -1
- package/dist/parsers/omnistudio/process.d.ts.map +1 -1
- package/dist/parsers/omnistudio/process.js +3 -2
- package/dist/parsers/omnistudio/process.js.map +1 -1
- package/dist/parsers/omnistudio/ui-card.d.ts.map +1 -1
- package/dist/parsers/omnistudio/ui-card.js +3 -2
- package/dist/parsers/omnistudio/ui-card.js.map +1 -1
- package/dist/parsers/rules/custom-metadata-type.yml +7 -0
- package/dist/parsers/rules/custom-setting.yml +24 -0
- package/dist/parsers/vlocity/common.d.ts +16 -0
- package/dist/parsers/vlocity/common.d.ts.map +1 -1
- package/dist/parsers/vlocity/common.js +62 -0
- package/dist/parsers/vlocity/common.js.map +1 -1
- package/dist/parsers/vlocity/data-raptor.d.ts.map +1 -1
- package/dist/parsers/vlocity/data-raptor.js +43 -14
- package/dist/parsers/vlocity/data-raptor.js.map +1 -1
- package/dist/render/__tests__/sarif.test.js +1 -4
- package/dist/render/__tests__/sarif.test.js.map +1 -1
- package/dist/render/sarif.d.ts.map +1 -1
- package/dist/render/sarif.js +1 -3
- package/dist/render/sarif.js.map +1 -1
- package/dist/storage/__tests__/analysis-tables-migration.test.js +1 -1
- package/dist/storage/__tests__/graph-store.test.js +13 -0
- package/dist/storage/__tests__/graph-store.test.js.map +1 -1
- package/dist/storage/__tests__/migrations.test.js +4 -4
- package/dist/storage/__tests__/rename-stability.test.js.map +1 -1
- package/dist/storage/__tests__/sync-generation.test.d.ts +2 -0
- package/dist/storage/__tests__/sync-generation.test.d.ts.map +1 -0
- package/dist/storage/__tests__/sync-generation.test.js +159 -0
- package/dist/storage/__tests__/sync-generation.test.js.map +1 -0
- package/dist/storage/interfaces.d.ts +57 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/sqlite/graph-store.d.ts +15 -1
- package/dist/storage/sqlite/graph-store.d.ts.map +1 -1
- package/dist/storage/sqlite/graph-store.js +78 -0
- package/dist/storage/sqlite/graph-store.js.map +1 -1
- package/dist/storage/sqlite/migrations.d.ts.map +1 -1
- package/dist/storage/sqlite/migrations.js +19 -0
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/storage/sqlite/rename-stability.d.ts +1 -1
- package/dist/storage/sqlite/rename-stability.d.ts.map +1 -1
- package/dist/storage/sqlite/rename-stability.js +2 -6
- package/dist/storage/sqlite/rename-stability.js.map +1 -1
- package/package.json +1 -1
|
@@ -11,10 +11,11 @@ import { EmbeddingQueue } from "../embedding/index.js";
|
|
|
11
11
|
import { resolveOrg } from "../extractors/live-org/auth.js";
|
|
12
12
|
import { bulkRetrieve } from "../extractors/live-org/bulk-retrieve.js";
|
|
13
13
|
import { probeCapabilities } from "../extractors/live-org/capabilities.js";
|
|
14
|
-
import {
|
|
15
|
-
import { startLivenessProbe, } from "../extractors/live-org/liveness.js";
|
|
14
|
+
import { startLivenessProbe } from "../extractors/live-org/liveness.js";
|
|
16
15
|
import { dataPool, metadataPool, toolingPool } from "../extractors/live-org/rate-limit.js";
|
|
16
|
+
import { iterChanges } from "../extractors/live-org/source-member.js";
|
|
17
17
|
import { loadAllRules } from "../parsers/rules/_loader.js";
|
|
18
|
+
import { evaluateDeletionGuard, resolveMaxDropRatio } from "./detect-deletions-guard.js";
|
|
18
19
|
const NOOP_LOG = {
|
|
19
20
|
debug: () => { },
|
|
20
21
|
info: () => { },
|
|
@@ -112,7 +113,83 @@ function unwrapApexEnvelope(content) {
|
|
|
112
113
|
}
|
|
113
114
|
return { body: content };
|
|
114
115
|
}
|
|
115
|
-
|
|
116
|
+
/** Maps the Vlocity runner's emitted memberType (the bare VlocityDataPackType)
|
|
117
|
+
* to the parser-registry key the four rich Vlocity parsers register under.
|
|
118
|
+
* Only these four DataPack types have dedicated parsers; the other ~44 fall
|
|
119
|
+
* through to the opaque node parser (node-only, by design). */
|
|
120
|
+
const VLOCITY_MEMBER_TO_PARSER = {
|
|
121
|
+
DataRaptor: "VlocityDataRaptor",
|
|
122
|
+
VlocityDataRaptor: "VlocityDataRaptor",
|
|
123
|
+
IntegrationProcedure: "VlocityIntegrationProcedure",
|
|
124
|
+
VlocityIntegrationProcedure: "VlocityIntegrationProcedure",
|
|
125
|
+
OmniScript: "VlocityOmniScript",
|
|
126
|
+
VlocityOmniScript: "VlocityOmniScript",
|
|
127
|
+
VlocityCard: "VlocityCard",
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Build the text embedded for a node's semantic vector. Beyond the
|
|
131
|
+
* label/qname/description baseline, this folds in CONFIG VALUES so
|
|
132
|
+
* `find_similar` matches on what a config artifact actually resolves to —
|
|
133
|
+
* a Custom Metadata record's field values, a Custom Label's text, a Custom
|
|
134
|
+
* Setting's rows. Without this, searching "endpoint for product push" can't
|
|
135
|
+
* reach `End_Point_Urls.ProductPush_PLDT` because only its name was embedded.
|
|
136
|
+
* Exported for tests.
|
|
137
|
+
*/
|
|
138
|
+
export function buildEmbedText(label, qname, attributes) {
|
|
139
|
+
const a = attributes ?? {};
|
|
140
|
+
const parts = [`${label}: ${qname}`];
|
|
141
|
+
if (typeof a.description === "string" && a.description)
|
|
142
|
+
parts.push(a.description);
|
|
143
|
+
// Custom Metadata record field values: [{field,value}] | {field:value} | object.
|
|
144
|
+
const values = a.values;
|
|
145
|
+
if (Array.isArray(values)) {
|
|
146
|
+
for (const v of values) {
|
|
147
|
+
const f = v.field;
|
|
148
|
+
const val = v.value;
|
|
149
|
+
if (f != null && val != null)
|
|
150
|
+
parts.push(`${String(f)} = ${stringifyConfigVal(val)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (values && typeof values === "object") {
|
|
154
|
+
for (const [k, v] of Object.entries(values)) {
|
|
155
|
+
if (v != null && v !== "")
|
|
156
|
+
parts.push(`${k} = ${stringifyConfigVal(v)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Custom Label text value.
|
|
160
|
+
if (typeof a.value === "string" && a.value)
|
|
161
|
+
parts.push(a.value);
|
|
162
|
+
// Custom Setting rows: array of row objects (field=value per row).
|
|
163
|
+
const rows = a.customSettingRows;
|
|
164
|
+
if (Array.isArray(rows)) {
|
|
165
|
+
for (const row of rows) {
|
|
166
|
+
if (row && typeof row === "object") {
|
|
167
|
+
const kv = Object.entries(row)
|
|
168
|
+
.filter(([, v]) => v != null && v !== "")
|
|
169
|
+
.map(([k, v]) => `${k}=${stringifyConfigVal(v)}`)
|
|
170
|
+
.join(", ");
|
|
171
|
+
if (kv)
|
|
172
|
+
parts.push(kv);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return parts.join("\n");
|
|
177
|
+
}
|
|
178
|
+
function stringifyConfigVal(v) {
|
|
179
|
+
if (v == null)
|
|
180
|
+
return "";
|
|
181
|
+
if (typeof v === "object") {
|
|
182
|
+
try {
|
|
183
|
+
return JSON.stringify(v);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return String(v);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return String(v);
|
|
190
|
+
}
|
|
191
|
+
/** Exported for tests — maps a RawMember to the registered parser's input shape. */
|
|
192
|
+
export function adaptParserInput(ref, content) {
|
|
116
193
|
switch (ref.memberType) {
|
|
117
194
|
case "ApexClass": {
|
|
118
195
|
const { body, metaXml } = unwrapApexEnvelope(content);
|
|
@@ -167,10 +244,24 @@ function adaptParserInput(ref, content) {
|
|
|
167
244
|
}
|
|
168
245
|
return { type: ref.memberType, input: { name: ref.memberName, metadata } };
|
|
169
246
|
}
|
|
247
|
+
// The Vlocity runner emits the bare VlocityDataPackType as memberType
|
|
248
|
+
// ("DataRaptor", "IntegrationProcedure", "OmniScript", "VlocityCard"),
|
|
249
|
+
// but the four rich parsers register under "Vlocity"-prefixed type names
|
|
250
|
+
// ("VlocityDataRaptor", …). Without this mapping the runner's members
|
|
251
|
+
// never matched a case here and fell through to the opaque fallback —
|
|
252
|
+
// shallow nodes with zero DR_/IP_/OS_ edges. Only "VlocityCard" happened
|
|
253
|
+
// to align on both sides, which is why a real ingest produced VC edges
|
|
254
|
+
// but no DataRaptor/IP/OmniScript lineage. The golden tests call the
|
|
255
|
+
// parsers directly, so this runner→dispatch seam was never covered.
|
|
256
|
+
// The legacy "Vlocity*"-prefixed keys are kept too in case any caller
|
|
257
|
+
// (or a future filesystem source) emits them.
|
|
258
|
+
case "DataRaptor":
|
|
170
259
|
case "VlocityDataRaptor":
|
|
260
|
+
case "IntegrationProcedure":
|
|
171
261
|
case "VlocityIntegrationProcedure":
|
|
172
|
-
case "
|
|
173
|
-
case "VlocityOmniScript":
|
|
262
|
+
case "OmniScript":
|
|
263
|
+
case "VlocityOmniScript":
|
|
264
|
+
case "VlocityCard": {
|
|
174
265
|
let datapack = content;
|
|
175
266
|
try {
|
|
176
267
|
datapack = JSON.parse(content);
|
|
@@ -178,12 +269,53 @@ function adaptParserInput(ref, content) {
|
|
|
178
269
|
catch {
|
|
179
270
|
/* keep raw */
|
|
180
271
|
}
|
|
181
|
-
return {
|
|
272
|
+
return {
|
|
273
|
+
type: VLOCITY_MEMBER_TO_PARSER[ref.memberType] ?? ref.memberType,
|
|
274
|
+
input: { name: ref.memberName, datapack },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
default: {
|
|
278
|
+
// Long-tail metadata types: route to a registered parser when one
|
|
279
|
+
// exists (the 21 rule-based YAML parsers register under their raw
|
|
280
|
+
// metadata type name, e.g. "Layout", "Dashboard"), otherwise fall
|
|
281
|
+
// back to the opaque node parser so the member still becomes a
|
|
282
|
+
// graph node. Before this fallback every generic-extractor member
|
|
283
|
+
// was silently dropped here — extracted, logged as ✓, and never
|
|
284
|
+
// merged — which left grant/reference edges dangling (e.g. 59k
|
|
285
|
+
// GRANTS_TAB_ACCESS edges pointing at CustomTab nodes that were
|
|
286
|
+
// never created).
|
|
287
|
+
if (parserRegistry.for(ref.memberType)) {
|
|
288
|
+
return { type: ref.memberType, input: genericRuleInput(ref.memberName, content) };
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
type: "OpaqueMetadata",
|
|
292
|
+
input: { metadataType: ref.memberType, name: ref.memberName, raw: content },
|
|
293
|
+
};
|
|
182
294
|
}
|
|
183
|
-
default:
|
|
184
|
-
return null;
|
|
185
295
|
}
|
|
186
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Build the `{ name, xml, ...siblings }` input shape the rule-based parsers'
|
|
299
|
+
* `xml-string` coercion expects. Filesystem sources hand us raw XML; the
|
|
300
|
+
* live generic extractor hands us the JSON-serialized `metadata.read()`
|
|
301
|
+
* object — whose keys mirror the parsed XML root, so they can be promoted
|
|
302
|
+
* to sibling keys directly (coerceInput merges siblings into the record).
|
|
303
|
+
*/
|
|
304
|
+
function genericRuleInput(name, content) {
|
|
305
|
+
const s = (content ?? "").trimStart();
|
|
306
|
+
if (s.startsWith("<"))
|
|
307
|
+
return { name, xml: content };
|
|
308
|
+
let parsed = {};
|
|
309
|
+
try {
|
|
310
|
+
const j = JSON.parse(s);
|
|
311
|
+
if (j && typeof j === "object" && !Array.isArray(j))
|
|
312
|
+
parsed = j;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
/* not JSON — leave record empty; the node still gets name + label */
|
|
316
|
+
}
|
|
317
|
+
return { xml: "", ...parsed, name };
|
|
318
|
+
}
|
|
187
319
|
export async function liveIngest(opts) {
|
|
188
320
|
const logger = opts.logger ?? NOOP_LOG;
|
|
189
321
|
// Load declarative rule parsers (idempotent).
|
|
@@ -206,617 +338,712 @@ export async function liveIngest(opts) {
|
|
|
206
338
|
apiVersion,
|
|
207
339
|
createdAt: now,
|
|
208
340
|
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
//
|
|
341
|
+
// W1.5-08: flag the org row as having an in-flight ingest before any
|
|
342
|
+
// extractor work. Done after upsertOrg so the row definitely exists.
|
|
343
|
+
// The matching markSyncComplete / markSyncFailed calls below are wired
|
|
344
|
+
// into a try/catch around the whole ingest body so concurrent MCP
|
|
345
|
+
// readers always see in_progress flip back to 0 even when ingest throws.
|
|
346
|
+
// Failure to record sync-start is non-fatal — log and continue rather
|
|
347
|
+
// than aborting an ingest because the bookkeeping column is unhappy.
|
|
348
|
+
const ingestStartedIso = new Date(now).toISOString();
|
|
213
349
|
try {
|
|
214
|
-
|
|
215
|
-
const discovered = await discoverMetadataTypes(resolved.conn, apiVersion);
|
|
216
|
-
logger.info("live-ingest: discovered metadata types", { count: discovered.length });
|
|
350
|
+
graph.markSyncStarted(resolved.orgId, ingestStartedIso);
|
|
217
351
|
}
|
|
218
352
|
catch (e) {
|
|
219
|
-
logger.warn("live-ingest:
|
|
220
|
-
}
|
|
221
|
-
const existing = graph.getOrg(resolved.orgId);
|
|
222
|
-
const requestedMode = opts.mode ?? "auto";
|
|
223
|
-
let mode;
|
|
224
|
-
if (requestedMode === "auto") {
|
|
225
|
-
mode = existing?.lastSyncedAt && caps.sourceTracking ? "incremental" : "full";
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
mode = requestedMode;
|
|
353
|
+
logger.warn("live-ingest: markSyncStarted failed", { err: e.message });
|
|
229
354
|
}
|
|
230
|
-
|
|
355
|
+
// W1.5-08: the rest of the ingest runs inside this try so a thrown
|
|
356
|
+
// exception still flips `sync_in_progress` back to 0 via markSyncFailed.
|
|
357
|
+
// Per-resolver try/catch isolation inside bulkRetrieve / live-ingest is
|
|
358
|
+
// preserved — this wrapper only catches exceptions that escape *all* of
|
|
359
|
+
// those inner handlers (i.e. genuinely fatal failures).
|
|
360
|
+
try {
|
|
361
|
+
const caps = await probeCapabilities(resolved.conn);
|
|
362
|
+
logger.info("live-ingest: probed capabilities", { caps });
|
|
363
|
+
// Discovery is also invoked implicitly inside bulkRetrieve, but logging the
|
|
364
|
+
// type-count up-front gives operators a quick coverage signal.
|
|
231
365
|
try {
|
|
232
|
-
|
|
366
|
+
const { discoverMetadataTypes } = await import("../extractors/live-org/discovery.js");
|
|
367
|
+
const discovered = await discoverMetadataTypes(resolved.conn, apiVersion);
|
|
368
|
+
logger.info("live-ingest: discovered metadata types", { count: discovered.length });
|
|
233
369
|
}
|
|
234
370
|
catch (e) {
|
|
235
|
-
logger.warn("live-ingest:
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
let membersProcessed = 0;
|
|
239
|
-
let parseErrors = 0;
|
|
240
|
-
let deletions = 0;
|
|
241
|
-
// Set in the full-sync branch if the bulkRetrieve stream itself throws.
|
|
242
|
-
// Detect-deletions reads it post-fan-out to bail out instead of mass-
|
|
243
|
-
// wiping nodes that were never visited due to the abort.
|
|
244
|
-
let streamAborted = false;
|
|
245
|
-
const touchedQnames = new Set();
|
|
246
|
-
// Skip/warning report collected across all branches. Populated by the
|
|
247
|
-
// full-sync fan-out via bulkRetrieve's onError callback; incremental mode
|
|
248
|
-
// currently emits no entries (single-stream iterChanges has no per-source
|
|
249
|
-
// failure surface). Returned on LiveIngestResult.warnings so MCP consumers
|
|
250
|
-
// can read what was skipped without parsing stdout.
|
|
251
|
-
const skipReport = { skips: [] };
|
|
252
|
-
const parseCtxBase = {
|
|
253
|
-
orgId: resolved.orgId,
|
|
254
|
-
parseTimestamp: new Date(now).toISOString(),
|
|
255
|
-
namespace: null,
|
|
256
|
-
logger,
|
|
257
|
-
};
|
|
258
|
-
const embedQueue = opts.vectorStore
|
|
259
|
-
? new EmbeddingQueue({
|
|
260
|
-
vectorStore: opts.vectorStore,
|
|
261
|
-
onError: (err) => logger.warn("live-ingest: embedding batch failed", { err: err.message }),
|
|
262
|
-
})
|
|
263
|
-
: null;
|
|
264
|
-
const handleParsed = (parsed) => {
|
|
265
|
-
if (parsed.nodes.length) {
|
|
266
|
-
graph.mergeNodes(parsed.nodes);
|
|
267
|
-
for (const n of parsed.nodes)
|
|
268
|
-
touchedQnames.add(String(n.qualifiedName));
|
|
269
|
-
}
|
|
270
|
-
if (parsed.edges.length)
|
|
271
|
-
graph.mergeEdges(parsed.edges);
|
|
272
|
-
if (parsed.snippets?.length) {
|
|
273
|
-
graph.transaction(() => {
|
|
274
|
-
for (const s of parsed.snippets ?? []) {
|
|
275
|
-
graph.upsertSnippet(s);
|
|
276
|
-
}
|
|
277
|
-
});
|
|
371
|
+
logger.warn("live-ingest: metadata.describe failed", { err: e.message });
|
|
278
372
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
qname: String(n.qualifiedName),
|
|
285
|
-
text,
|
|
286
|
-
orgId: String(n.orgId),
|
|
287
|
-
label: n.label,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
373
|
+
const existing = graph.getOrg(resolved.orgId);
|
|
374
|
+
const requestedMode = opts.mode ?? "auto";
|
|
375
|
+
let mode;
|
|
376
|
+
if (requestedMode === "auto") {
|
|
377
|
+
mode = existing?.lastSyncedAt && caps.sourceTracking ? "incremental" : "full";
|
|
290
378
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const processOne = async (ref, content) => {
|
|
294
|
-
const qnameForLog = `${ref.memberType}:${ref.memberName}`;
|
|
295
|
-
if (ref.obsolete) {
|
|
296
|
-
// Build the qualified name same way parsers would: best-effort by member name.
|
|
297
|
-
const qname = asQualifiedName(`${ref.memberType}:${ref.memberName}`);
|
|
298
|
-
if (debugProcess)
|
|
299
|
-
console.log(`ingest: [trace] delete ← ${qnameForLog}`);
|
|
300
|
-
graph.deleteEdgesFor(resolved.orgId, qname);
|
|
301
|
-
graph.deleteNode(resolved.orgId, qname);
|
|
302
|
-
if (debugProcess)
|
|
303
|
-
console.log(`ingest: [trace] delete ✓ ${qnameForLog}`);
|
|
304
|
-
deletions += 1;
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
const adapted = adaptParserInput(ref, content);
|
|
308
|
-
if (!adapted)
|
|
309
|
-
return;
|
|
310
|
-
const parser = parserRegistry.for(adapted.type);
|
|
311
|
-
if (!parser)
|
|
312
|
-
return;
|
|
313
|
-
try {
|
|
314
|
-
const ctx = {
|
|
315
|
-
...parseCtxBase,
|
|
316
|
-
sourceUri: ref.sourceUri,
|
|
317
|
-
namespace: ref.namespace ?? null,
|
|
318
|
-
};
|
|
319
|
-
if (debugProcess)
|
|
320
|
-
console.log(`ingest: [trace] parse ← ${qnameForLog}`);
|
|
321
|
-
const result = await parser.parse(adapted.input, ctx);
|
|
322
|
-
if (debugProcess)
|
|
323
|
-
console.log(`ingest: [trace] parse ✓ ${qnameForLog} (nodes=${result.nodes.length} edges=${result.edges.length})`);
|
|
324
|
-
if (debugProcess)
|
|
325
|
-
console.log(`ingest: [trace] graph-merge ← ${qnameForLog}`);
|
|
326
|
-
handleParsed(result);
|
|
327
|
-
if (debugProcess)
|
|
328
|
-
console.log(`ingest: [trace] graph-merge ✓ ${qnameForLog}`);
|
|
329
|
-
membersProcessed += 1;
|
|
379
|
+
else {
|
|
380
|
+
mode = requestedMode;
|
|
330
381
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
err: e.message
|
|
337
|
-
});
|
|
338
|
-
if (debugProcess) {
|
|
339
|
-
console.error(`ingest: [trace] FAILURE ${qnameForLog}: ${e.message}\n${e.stack}`);
|
|
382
|
+
if (!opts.skipSnapshot && opts.snapshotStore) {
|
|
383
|
+
try {
|
|
384
|
+
opts.snapshotStore.createSnapshot(resolved.orgId, `pre-sync-${new Date(now).toISOString()}`, true);
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
logger.warn("live-ingest: snapshot failed", { err: e.message });
|
|
340
388
|
}
|
|
341
389
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
const {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
390
|
+
let membersProcessed = 0;
|
|
391
|
+
let parseErrors = 0;
|
|
392
|
+
let deletions = 0;
|
|
393
|
+
// Set in the full-sync branch if the bulkRetrieve stream itself throws.
|
|
394
|
+
// Detect-deletions reads it post-fan-out to bail out instead of mass-
|
|
395
|
+
// wiping nodes that were never visited due to the abort.
|
|
396
|
+
let streamAborted = false;
|
|
397
|
+
const touchedQnames = new Set();
|
|
398
|
+
// W1.5-07: maintain per-label touched-qnames sets in parallel with the
|
|
399
|
+
// flat `touchedQnames` set so the deletion-sweep guard can compute
|
|
400
|
+
// priorCount/touchedCount per label without re-deriving labels later.
|
|
401
|
+
const touchedQnamesByLabel = new Map();
|
|
402
|
+
// Skip/warning report collected across all branches. Populated by the
|
|
403
|
+
// full-sync fan-out via bulkRetrieve's onError callback; incremental mode
|
|
404
|
+
// currently emits no entries (single-stream iterChanges has no per-source
|
|
405
|
+
// failure surface). Returned on LiveIngestResult.warnings so MCP consumers
|
|
406
|
+
// can read what was skipped without parsing stdout.
|
|
407
|
+
const skipReport = { skips: [] };
|
|
408
|
+
// Phase 1.5 W1.5-02: namespaced wedge warnings populated by bulkRetrieve's
|
|
409
|
+
// soft-isolate path (`wedge:<source>:<stage>:<detail>`). Concatenated with
|
|
410
|
+
// the legacy `${label}: ${reason}` strings from skipReport.skips into the
|
|
411
|
+
// final LiveIngestResult.warnings array. See bulk-retrieve.ts WedgeCoordinator.
|
|
412
|
+
const wedgeWarnings = [];
|
|
413
|
+
const parseCtxBase = {
|
|
414
|
+
orgId: resolved.orgId,
|
|
415
|
+
parseTimestamp: new Date(now).toISOString(),
|
|
416
|
+
namespace: null,
|
|
417
|
+
logger,
|
|
418
|
+
};
|
|
419
|
+
const embedQueue = opts.vectorStore
|
|
420
|
+
? new EmbeddingQueue({
|
|
421
|
+
vectorStore: opts.vectorStore,
|
|
422
|
+
onError: (err) => logger.warn("live-ingest: embedding batch failed", { err: err.message }),
|
|
423
|
+
})
|
|
424
|
+
: null;
|
|
425
|
+
const handleParsed = (parsed) => {
|
|
426
|
+
if (parsed.nodes.length) {
|
|
427
|
+
graph.mergeNodes(parsed.nodes);
|
|
428
|
+
for (const n of parsed.nodes) {
|
|
429
|
+
const qn = String(n.qualifiedName);
|
|
430
|
+
touchedQnames.add(qn);
|
|
431
|
+
// W1.5-07: also bucket by label for the per-label deletion guard.
|
|
432
|
+
let bucket = touchedQnamesByLabel.get(n.label);
|
|
433
|
+
if (!bucket) {
|
|
434
|
+
bucket = new Set();
|
|
435
|
+
touchedQnamesByLabel.set(n.label, bucket);
|
|
436
|
+
}
|
|
437
|
+
bucket.add(qn);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (parsed.edges.length)
|
|
441
|
+
graph.mergeEdges(parsed.edges);
|
|
442
|
+
if (parsed.snippets?.length) {
|
|
443
|
+
graph.transaction(() => {
|
|
444
|
+
for (const s of parsed.snippets ?? []) {
|
|
445
|
+
graph.upsertSnippet(s);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (embedQueue) {
|
|
450
|
+
for (const n of parsed.nodes) {
|
|
451
|
+
embedQueue.push({
|
|
452
|
+
qname: String(n.qualifiedName),
|
|
453
|
+
text: buildEmbedText(n.label, String(n.qualifiedName), n.attributes),
|
|
454
|
+
orgId: String(n.orgId),
|
|
455
|
+
label: n.label,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
const debugProcess = process.env.SFGRAPH_DEBUG_INGEST === "1";
|
|
461
|
+
const processOne = async (ref, content) => {
|
|
462
|
+
const qnameForLog = `${ref.memberType}:${ref.memberName}`;
|
|
463
|
+
if (ref.obsolete) {
|
|
464
|
+
// Build the qualified name same way parsers would: best-effort by member name.
|
|
465
|
+
const qname = asQualifiedName(`${ref.memberType}:${ref.memberName}`);
|
|
466
|
+
if (debugProcess)
|
|
467
|
+
console.log(`ingest: [trace] delete ← ${qnameForLog}`);
|
|
468
|
+
graph.deleteEdgesFor(resolved.orgId, qname);
|
|
469
|
+
graph.deleteNode(resolved.orgId, qname);
|
|
470
|
+
if (debugProcess)
|
|
471
|
+
console.log(`ingest: [trace] delete ✓ ${qnameForLog}`);
|
|
472
|
+
deletions += 1;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const adapted = adaptParserInput(ref, content);
|
|
476
|
+
if (!adapted)
|
|
477
|
+
return;
|
|
478
|
+
const parser = parserRegistry.for(adapted.type);
|
|
479
|
+
if (!parser)
|
|
480
|
+
return;
|
|
375
481
|
try {
|
|
376
|
-
const
|
|
377
|
-
const mcdCtx = {
|
|
482
|
+
const ctx = {
|
|
378
483
|
...parseCtxBase,
|
|
379
|
-
sourceUri:
|
|
484
|
+
sourceUri: ref.sourceUri,
|
|
485
|
+
namespace: ref.namespace ?? null,
|
|
380
486
|
};
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (mcd.edges.length > 0) {
|
|
393
|
-
graph.mergeEdges(mcd.edges);
|
|
394
|
-
}
|
|
395
|
-
logger.info("live-ingest: MCD baseline complete", {
|
|
396
|
-
edges: mcd.edges.length,
|
|
397
|
-
byType: mcd.byType,
|
|
398
|
-
});
|
|
487
|
+
if (debugProcess)
|
|
488
|
+
console.log(`ingest: [trace] parse ← ${qnameForLog}`);
|
|
489
|
+
const result = await parser.parse(adapted.input, ctx);
|
|
490
|
+
if (debugProcess)
|
|
491
|
+
console.log(`ingest: [trace] parse ✓ ${qnameForLog} (nodes=${result.nodes.length} edges=${result.edges.length})`);
|
|
492
|
+
if (debugProcess)
|
|
493
|
+
console.log(`ingest: [trace] graph-merge ← ${qnameForLog}`);
|
|
494
|
+
handleParsed(result);
|
|
495
|
+
if (debugProcess)
|
|
496
|
+
console.log(`ingest: [trace] graph-merge ✓ ${qnameForLog}`);
|
|
497
|
+
membersProcessed += 1;
|
|
399
498
|
}
|
|
400
499
|
catch (e) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const PROGRESS_TICK_MS = 5000; // emit a heartbeat at least every 5s
|
|
410
|
-
if (opts.onlyLabels && opts.onlyLabels.size > 0) {
|
|
411
|
-
console.log(`ingest: --only filter active (${opts.onlyLabels.size} source${opts.onlyLabels.size === 1 ? "" : "s"}): ${[...opts.onlyLabels].join(", ")}`);
|
|
412
|
-
}
|
|
413
|
-
// Debug mode: SFGRAPH_DEBUG_INGEST=1 enables a heartbeat timer that
|
|
414
|
-
// prints heap usage + seconds-since-last-activity every 10s. Critical
|
|
415
|
-
// for diagnosing silent ingest deaths (OOM, native segfault, hung SF
|
|
416
|
-
// call) where the normal progress log just stops without an error.
|
|
417
|
-
const debug = process.env.SFGRAPH_DEBUG_INGEST === "1";
|
|
418
|
-
let heartbeatTimer = null;
|
|
419
|
-
let keepAlive = null;
|
|
420
|
-
let livenessProbe = null;
|
|
421
|
-
// Track signal handlers we register so we can remove them in finally.
|
|
422
|
-
// Without this, every invocation of liveIngest in debug mode leaked one
|
|
423
|
-
// listener per signal (SIGTERM/SIGINT/SIGUSR2), eventually triggering
|
|
424
|
-
// MaxListenersExceededWarning and N-fold handler firing on first ^C in
|
|
425
|
-
// multi-org / programmatic flows. (Audit finding C2.)
|
|
426
|
-
const installedSignalHandlers = [];
|
|
427
|
-
// streamAborted (hoisted at function scope) gates the post-fan-out
|
|
428
|
-
// detect-deletions block. If the bulkRetrieve stream itself throws
|
|
429
|
-
// (vs individual sources fail-softing), we've only seen a fraction of
|
|
430
|
-
// the org and the `touchedQnames` set is not safe to use for stale-
|
|
431
|
-
// qname computation — without this guard a transient SF error would
|
|
432
|
-
// mass-delete the untouched 70% of the graph. (Audit finding C1.)
|
|
433
|
-
try {
|
|
434
|
-
// Keep-alive sentinel: a ref'd timer that exists purely to prevent the
|
|
435
|
-
// event loop from draining while we're awaiting Bottleneck-scheduled
|
|
436
|
-
// work. Bottleneck's local-datastore reservoir-refresh timer is unref'd
|
|
437
|
-
// internally, jsforce's HTTP keep-alive sockets idle between requests,
|
|
438
|
-
// and Node will happily exit(0) on a perfectly healthy pending Promise
|
|
439
|
-
// if no ref'd handle is left.
|
|
440
|
-
keepAlive = setInterval(() => { }, 60_000);
|
|
441
|
-
// Background liveness probe. Polls conn.identity() every 30s with a
|
|
442
|
-
// 10s deadline; after two consecutive failures it logs a prominent
|
|
443
|
-
// "CONNECTION LOST" warning so the user knows why surviving
|
|
444
|
-
// extractors are about to time out one by one. We intentionally do
|
|
445
|
-
// NOT abort the ingest from here — per-call 60s timeouts (added in
|
|
446
|
-
// 1.1.3) already cap exposure, and aborting mid-flight would need
|
|
447
|
-
// an AbortSignal threaded through every extractor for marginal
|
|
448
|
-
// benefit. The probe's job is observability, not control flow.
|
|
449
|
-
// Disable via SFGRAPH_NO_LIVENESS_PROBE=1 for hermetic test runs
|
|
450
|
-
// where the mock connection has no identity() implementation.
|
|
451
|
-
if (process.env.SFGRAPH_NO_LIVENESS_PROBE !== "1") {
|
|
452
|
-
livenessProbe = startLivenessProbe(resolved.conn);
|
|
453
|
-
}
|
|
454
|
-
// Wall-clock heartbeat — always on. Without this, silent phases (where
|
|
455
|
-
// all in-window sources are awaiting Bottleneck-queued metadata reads
|
|
456
|
-
// without yielding) look indistinguishable from a hung process. The
|
|
457
|
-
// per-record progress tick at the bottom of the fan-out loop only
|
|
458
|
-
// fires when a record arrives; this fires on a real timer regardless.
|
|
459
|
-
// Debug mode adds heap/rss; baseline shows processed/lastSource/idle.
|
|
460
|
-
const showPoolCounters = debug || process.env.SFGRAPH_DEBUG_POOLS === "1";
|
|
461
|
-
heartbeatTimer = setInterval(() => {
|
|
462
|
-
const idleSec = Math.round((Date.now() - lastActivityAt) / 1000);
|
|
463
|
-
if (idleSec < 10)
|
|
464
|
-
return; // suppress chatter when records are flowing
|
|
465
|
-
if (debug) {
|
|
466
|
-
const mem = process.memoryUsage();
|
|
467
|
-
const heapMb = Math.round(mem.heapUsed / 1024 / 1024);
|
|
468
|
-
const rssMb = Math.round(mem.rss / 1024 / 1024);
|
|
469
|
-
const externalMb = Math.round(mem.external / 1024 / 1024);
|
|
470
|
-
console.log(`ingest: [heartbeat] processed=${progressCount} lastSource=${lastMemberLabel} idle=${idleSec}s heap=${heapMb}MB rss=${rssMb}MB ext=${externalMb}MB`);
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
console.log(`ingest: …still working — processed=${progressCount} idle=${idleSec}s lastSource=${lastMemberLabel}`);
|
|
500
|
+
parseErrors += 1;
|
|
501
|
+
logger.warn("live-ingest: parser failure", {
|
|
502
|
+
type: ref.memberType,
|
|
503
|
+
name: ref.memberName,
|
|
504
|
+
err: e.message,
|
|
505
|
+
});
|
|
506
|
+
if (debugProcess) {
|
|
507
|
+
console.error(`ingest: [trace] FAILURE ${qnameForLog}: ${e.message}\n${e.stack}`);
|
|
474
508
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
if (mode === "incremental") {
|
|
512
|
+
const sinceIso = existing?.lastSyncedAt
|
|
513
|
+
? new Date(existing.lastSyncedAt).toISOString()
|
|
514
|
+
: new Date(now - 24 * 3600 * 1000).toISOString();
|
|
515
|
+
for await (const ref of iterChanges(resolved.conn, resolved.orgId, sinceIso)) {
|
|
516
|
+
// For incremental, content is best fetched on-demand; for deletions we skip content.
|
|
517
|
+
await processOne(ref, "");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
// The outer iteration is also wrapped: bulkRetrieve uses fail-soft per
|
|
522
|
+
// source, but a top-level catch protects against any iterator-protocol
|
|
523
|
+
// surprises (e.g. generator throws during `next()` before yielding).
|
|
524
|
+
// Read live pool caps so the log reflects --tooling-pool/--metadata-pool/
|
|
525
|
+
// --data-pool overrides or SFGRAPH_*_POOL env vars rather than hardcoded
|
|
526
|
+
// defaults.
|
|
527
|
+
const { toolingPool, metadataPool, dataPool } = await import("../extractors/live-org/rate-limit.js");
|
|
528
|
+
const tCap = toolingPool._store.storeOptions.maxConcurrent;
|
|
529
|
+
const mCap = metadataPool._store.storeOptions.maxConcurrent;
|
|
530
|
+
const dCap = dataPool
|
|
531
|
+
._store.storeOptions.maxConcurrent;
|
|
532
|
+
console.log(`ingest: starting full sync (Tooling pool ${tCap} / Metadata pool ${mCap} / Data pool ${dCap} concurrent)`);
|
|
533
|
+
// MCD baseline runs BEFORE the parser fan-out so any (src, dst, REFERENCES)
|
|
534
|
+
// edges it writes can be overwritten if a real parser produces the same
|
|
535
|
+
// shape later. The MCD path uses generic REFERENCES while parsers use
|
|
536
|
+
// specific rel-types — so in practice MCD and parsed coexist in
|
|
537
|
+
// different edge tables rather than competing for the same row, but
|
|
538
|
+
// the pre-fan-out ordering preserves the "parsed wins" guarantee for
|
|
539
|
+
// the (rare) case where a parser does emit REFERENCES.
|
|
540
|
+
if (!opts.disableMcdBaseline) {
|
|
541
|
+
try {
|
|
542
|
+
const { runMcdBaseline } = await import("../extractors/live-org/extractors/mcd-baseline.js");
|
|
543
|
+
const mcdCtx = {
|
|
544
|
+
...parseCtxBase,
|
|
545
|
+
sourceUri: "mcd-baseline://tooling-soql",
|
|
546
|
+
};
|
|
547
|
+
const mcd = await runMcdBaseline(resolved.conn, {
|
|
548
|
+
orgId: resolved.orgId,
|
|
549
|
+
ctx: mcdCtx,
|
|
550
|
+
onError: (label, err) => {
|
|
551
|
+
skipReport.skips.push({
|
|
552
|
+
label,
|
|
553
|
+
reason: err.message,
|
|
554
|
+
category: "unknown",
|
|
555
|
+
});
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
if (mcd.edges.length > 0) {
|
|
559
|
+
graph.mergeEdges(mcd.edges);
|
|
493
560
|
}
|
|
561
|
+
logger.info("live-ingest: MCD baseline complete", {
|
|
562
|
+
edges: mcd.edges.length,
|
|
563
|
+
byType: mcd.byType,
|
|
564
|
+
});
|
|
494
565
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
console.error(`ingest: [debug] SIGUSR2 — heap=${Math.round(mem.heapUsed / 1024 / 1024)}MB rss=${Math.round(mem.rss / 1024 / 1024)}MB lastSource=${lastMemberLabel}`);
|
|
508
|
-
};
|
|
509
|
-
installedSignalHandlers.push({ sig: "SIGTERM", fn: onSig("SIGTERM") });
|
|
510
|
-
installedSignalHandlers.push({ sig: "SIGINT", fn: onSig("SIGINT") });
|
|
511
|
-
installedSignalHandlers.push({ sig: "SIGUSR2", fn: onUsr2 });
|
|
512
|
-
for (const h of installedSignalHandlers)
|
|
513
|
-
process.on(h.sig, h.fn);
|
|
566
|
+
catch (e) {
|
|
567
|
+
logger.warn("live-ingest: MCD baseline failed", { err: e.message });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const fanOutStart = Date.now();
|
|
571
|
+
let progressCount = 0;
|
|
572
|
+
let lastTickAt = Date.now();
|
|
573
|
+
let lastActivityAt = Date.now();
|
|
574
|
+
let lastMemberLabel = "(none)";
|
|
575
|
+
const PROGRESS_TICK_MS = 5000; // emit a heartbeat at least every 5s
|
|
576
|
+
if (opts.onlyLabels && opts.onlyLabels.size > 0) {
|
|
577
|
+
console.log(`ingest: --only filter active (${opts.onlyLabels.size} source${opts.onlyLabels.size === 1 ? "" : "s"}): ${[...opts.onlyLabels].join(", ")}`);
|
|
514
578
|
}
|
|
579
|
+
// Debug mode: SFGRAPH_DEBUG_INGEST=1 enables a heartbeat timer that
|
|
580
|
+
// prints heap usage + seconds-since-last-activity every 10s. Critical
|
|
581
|
+
// for diagnosing silent ingest deaths (OOM, native segfault, hung SF
|
|
582
|
+
// call) where the normal progress log just stops without an error.
|
|
583
|
+
const debug = process.env.SFGRAPH_DEBUG_INGEST === "1";
|
|
584
|
+
let heartbeatTimer = null;
|
|
585
|
+
let keepAlive = null;
|
|
586
|
+
let livenessProbe = null;
|
|
587
|
+
// Track signal handlers we register so we can remove them in finally.
|
|
588
|
+
// Without this, every invocation of liveIngest in debug mode leaked one
|
|
589
|
+
// listener per signal (SIGTERM/SIGINT/SIGUSR2), eventually triggering
|
|
590
|
+
// MaxListenersExceededWarning and N-fold handler firing on first ^C in
|
|
591
|
+
// multi-org / programmatic flows. (Audit finding C2.)
|
|
592
|
+
const installedSignalHandlers = [];
|
|
593
|
+
// streamAborted (hoisted at function scope) gates the post-fan-out
|
|
594
|
+
// detect-deletions block. If the bulkRetrieve stream itself throws
|
|
595
|
+
// (vs individual sources fail-softing), we've only seen a fraction of
|
|
596
|
+
// the org and the `touchedQnames` set is not safe to use for stale-
|
|
597
|
+
// qname computation — without this guard a transient SF error would
|
|
598
|
+
// mass-delete the untouched 70% of the graph. (Audit finding C1.)
|
|
515
599
|
try {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
600
|
+
// Keep-alive sentinel: a ref'd timer that exists purely to prevent the
|
|
601
|
+
// event loop from draining while we're awaiting Bottleneck-scheduled
|
|
602
|
+
// work. Bottleneck's local-datastore reservoir-refresh timer is unref'd
|
|
603
|
+
// internally, jsforce's HTTP keep-alive sockets idle between requests,
|
|
604
|
+
// and Node will happily exit(0) on a perfectly healthy pending Promise
|
|
605
|
+
// if no ref'd handle is left.
|
|
606
|
+
keepAlive = setInterval(() => { }, 60_000);
|
|
607
|
+
// Background liveness probe. Polls conn.identity() every 30s with a
|
|
608
|
+
// 10s deadline; after two consecutive failures it logs a prominent
|
|
609
|
+
// "CONNECTION LOST" warning so the user knows why surviving
|
|
610
|
+
// extractors are about to time out one by one. We intentionally do
|
|
611
|
+
// NOT abort the ingest from here — per-call 60s timeouts (added in
|
|
612
|
+
// 1.1.3) already cap exposure, and aborting mid-flight would need
|
|
613
|
+
// an AbortSignal threaded through every extractor for marginal
|
|
614
|
+
// benefit. The probe's job is observability, not control flow.
|
|
615
|
+
// Disable via SFGRAPH_NO_LIVENESS_PROBE=1 for hermetic test runs
|
|
616
|
+
// where the mock connection has no identity() implementation.
|
|
617
|
+
if (process.env.SFGRAPH_NO_LIVENESS_PROBE !== "1") {
|
|
618
|
+
livenessProbe = startLivenessProbe(resolved.conn);
|
|
619
|
+
}
|
|
620
|
+
// Wall-clock heartbeat — always on. Without this, silent phases (where
|
|
621
|
+
// all in-window sources are awaiting Bottleneck-queued metadata reads
|
|
622
|
+
// without yielding) look indistinguishable from a hung process. The
|
|
623
|
+
// per-record progress tick at the bottom of the fan-out loop only
|
|
624
|
+
// fires when a record arrives; this fires on a real timer regardless.
|
|
625
|
+
// Debug mode adds heap/rss; baseline shows processed/lastSource/idle.
|
|
626
|
+
const showPoolCounters = debug || process.env.SFGRAPH_DEBUG_POOLS === "1";
|
|
627
|
+
heartbeatTimer = setInterval(() => {
|
|
628
|
+
const idleSec = Math.round((Date.now() - lastActivityAt) / 1000);
|
|
629
|
+
if (idleSec < 10)
|
|
630
|
+
return; // suppress chatter when records are flowing
|
|
631
|
+
if (debug) {
|
|
632
|
+
const mem = process.memoryUsage();
|
|
633
|
+
const heapMb = Math.round(mem.heapUsed / 1024 / 1024);
|
|
634
|
+
const rssMb = Math.round(mem.rss / 1024 / 1024);
|
|
635
|
+
const externalMb = Math.round(mem.external / 1024 / 1024);
|
|
636
|
+
console.log(`ingest: [heartbeat] processed=${progressCount} lastSource=${lastMemberLabel} idle=${idleSec}s heap=${heapMb}MB rss=${rssMb}MB ext=${externalMb}MB`);
|
|
530
637
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
logger.warn("live-ingest: processOne failed", {
|
|
534
|
-
qname: `${member.ref.memberType}:${member.ref.memberName}`,
|
|
535
|
-
error: e.message,
|
|
536
|
-
});
|
|
638
|
+
else {
|
|
639
|
+
console.log(`ingest: …still working — processed=${progressCount} idle=${idleSec}s lastSource=${lastMemberLabel}`);
|
|
537
640
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
641
|
+
// Pool diagnostics: snapshot Bottleneck state so a wedge can be
|
|
642
|
+
// attributed concretely to reservoir starvation vs. stuck-in-flight
|
|
643
|
+
// jobs vs. pre-yield iterator hangs. Counts are synchronous; the
|
|
644
|
+
// reservoir read is async but fire-and-forget — by the time it
|
|
645
|
+
// resolves we just log it.
|
|
646
|
+
if (showPoolCounters) {
|
|
647
|
+
const snapshot = [
|
|
648
|
+
["tool", toolingPool],
|
|
649
|
+
["meta", metadataPool],
|
|
650
|
+
["data", dataPool],
|
|
651
|
+
];
|
|
652
|
+
for (const [name, p] of snapshot) {
|
|
653
|
+
const c = p.counts();
|
|
654
|
+
p.currentReservoir().then((reservoir) => {
|
|
655
|
+
console.log(`ingest: [pool ${name}] running=${c.RUNNING} executing=${c.EXECUTING} queued=${c.QUEUED} received=${c.RECEIVED} reservoir=${reservoir}`);
|
|
656
|
+
}, () => {
|
|
657
|
+
/* swallow — never let a diag failure kill ingest */
|
|
658
|
+
});
|
|
659
|
+
}
|
|
548
660
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
661
|
+
}, 10_000);
|
|
662
|
+
if (debug) {
|
|
663
|
+
console.log("ingest: [debug] SFGRAPH_DEBUG_INGEST=1 active — heartbeat every 10s");
|
|
664
|
+
// Signal handlers — surface what was running when the user (or OS)
|
|
665
|
+
// sends a kill so silent terminations have at least one breadcrumb.
|
|
666
|
+
const onSig = (sig) => () => {
|
|
667
|
+
console.error(`ingest: [debug] received ${sig} after ${Math.round((Date.now() - fanOutStart) / 1000)}s — processed=${progressCount} lastSource=${lastMemberLabel}`);
|
|
668
|
+
console.error(new Error(`signal:${sig}`).stack);
|
|
669
|
+
process.exitCode = 130;
|
|
670
|
+
};
|
|
671
|
+
const onUsr2 = () => {
|
|
672
|
+
const mem = process.memoryUsage();
|
|
673
|
+
console.error(`ingest: [debug] SIGUSR2 — heap=${Math.round(mem.heapUsed / 1024 / 1024)}MB rss=${Math.round(mem.rss / 1024 / 1024)}MB lastSource=${lastMemberLabel}`);
|
|
674
|
+
};
|
|
675
|
+
installedSignalHandlers.push({ sig: "SIGTERM", fn: onSig("SIGTERM") });
|
|
676
|
+
installedSignalHandlers.push({ sig: "SIGINT", fn: onSig("SIGINT") });
|
|
677
|
+
installedSignalHandlers.push({ sig: "SIGUSR2", fn: onUsr2 });
|
|
678
|
+
for (const h of installedSignalHandlers)
|
|
679
|
+
process.on(h.sig, h.fn);
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
for await (const member of bulkRetrieve(resolved.conn, caps, resolved.orgId, {
|
|
683
|
+
skipReport,
|
|
684
|
+
warnings: wedgeWarnings,
|
|
685
|
+
...(opts.onlyLabels ? { onlyLabels: opts.onlyLabels } : {}),
|
|
686
|
+
// The bulk-retrieve flag name follows the same convention as
|
|
687
|
+
// the bulk-retrieve's other opts; semantically it's "do the
|
|
688
|
+
// retrieve()" which is now on by default. Caller-side disable
|
|
689
|
+
// routes through this same boolean inverted.
|
|
690
|
+
...(opts.disableOmnistudioRetrieve ? {} : { enableOmnistudioRetrieve: true }),
|
|
691
|
+
// jsforce attaches the version it negotiated on conn.version; fall
|
|
692
|
+
// back to a sensible recent release if absent (mocks, etc.)
|
|
693
|
+
apiVersion: resolved.conn.version ?? "60.0",
|
|
694
|
+
})) {
|
|
695
|
+
try {
|
|
696
|
+
await processOne(member.ref, member.content);
|
|
697
|
+
}
|
|
698
|
+
catch (e) {
|
|
699
|
+
parseErrors += 1;
|
|
700
|
+
logger.warn("live-ingest: processOne failed", {
|
|
701
|
+
qname: `${member.ref.memberType}:${member.ref.memberName}`,
|
|
702
|
+
error: e.message,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
progressCount += 1;
|
|
706
|
+
lastActivityAt = Date.now();
|
|
707
|
+
lastMemberLabel = `${member.ref.memberType}:${member.ref.memberName}`;
|
|
708
|
+
if (progressCount % 200 === 0 || Date.now() - lastTickAt > PROGRESS_TICK_MS) {
|
|
709
|
+
const elapsedSec = Math.round((Date.now() - fanOutStart) / 1000);
|
|
710
|
+
const memSuffix = debug
|
|
711
|
+
? ` heap=${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`
|
|
712
|
+
: "";
|
|
713
|
+
console.log(`ingest: …${progressCount} members processed so far (${elapsedSec}s elapsed)${memSuffix}`);
|
|
714
|
+
lastTickAt = Date.now();
|
|
715
|
+
}
|
|
716
|
+
if (progressCount % 500 === 0) {
|
|
717
|
+
const gs = graph;
|
|
718
|
+
if (typeof gs.checkpoint === "function")
|
|
719
|
+
gs.checkpoint();
|
|
720
|
+
}
|
|
553
721
|
}
|
|
722
|
+
console.log(`ingest: fan-out complete (${progressCount} members in ${Math.round((Date.now() - fanOutStart) / 1000)}s)`);
|
|
723
|
+
const gs = graph;
|
|
724
|
+
if (typeof gs.checkpoint === "function")
|
|
725
|
+
gs.checkpoint();
|
|
726
|
+
}
|
|
727
|
+
catch (e) {
|
|
728
|
+
// The stream itself threw (not an individual source — those are
|
|
729
|
+
// captured by failSoft). Mark so detect-deletions stays its hand.
|
|
730
|
+
streamAborted = true;
|
|
731
|
+
logger.warn("live-ingest: bulkRetrieve stream aborted", {
|
|
732
|
+
error: e.message,
|
|
733
|
+
});
|
|
554
734
|
}
|
|
555
|
-
console.log(`ingest: fan-out complete (${progressCount} members in ${Math.round((Date.now() - fanOutStart) / 1000)}s)`);
|
|
556
|
-
const gs = graph;
|
|
557
|
-
if (typeof gs.checkpoint === "function")
|
|
558
|
-
gs.checkpoint();
|
|
559
735
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
736
|
+
finally {
|
|
737
|
+
if (heartbeatTimer)
|
|
738
|
+
clearInterval(heartbeatTimer);
|
|
739
|
+
if (keepAlive)
|
|
740
|
+
clearInterval(keepAlive);
|
|
741
|
+
livenessProbe?.stop();
|
|
742
|
+
for (const h of installedSignalHandlers)
|
|
743
|
+
process.off(h.sig, h.fn);
|
|
744
|
+
}
|
|
745
|
+
// End-of-run skip summary. Grouped by category so the remediation is
|
|
746
|
+
// targeted to the specific class of failure.
|
|
747
|
+
if (skipReport.skips.length > 0) {
|
|
748
|
+
printSkipSummary(skipReport, resolved.alias);
|
|
749
|
+
}
|
|
750
|
+
// Persist the skip report so --retry-skipped can read it next run.
|
|
751
|
+
// Always written (even when empty) so users can tell whether the last
|
|
752
|
+
// run had skips or just hasn't recorded any yet.
|
|
753
|
+
if (opts.skipReportPath) {
|
|
754
|
+
try {
|
|
755
|
+
const { writeFileSync, mkdirSync } = await import("node:fs");
|
|
756
|
+
const { dirname } = await import("node:path");
|
|
757
|
+
mkdirSync(dirname(opts.skipReportPath), { recursive: true });
|
|
758
|
+
writeFileSync(opts.skipReportPath, JSON.stringify({ recordedAt: Date.now(), orgId: resolved.orgId, ...skipReport }, null, 2), "utf8");
|
|
759
|
+
}
|
|
760
|
+
catch (e) {
|
|
761
|
+
logger.warn("live-ingest: skip report write failed", { err: e.message });
|
|
762
|
+
}
|
|
567
763
|
}
|
|
568
764
|
}
|
|
569
|
-
|
|
570
|
-
if (heartbeatTimer)
|
|
571
|
-
clearInterval(heartbeatTimer);
|
|
572
|
-
if (keepAlive)
|
|
573
|
-
clearInterval(keepAlive);
|
|
574
|
-
livenessProbe?.stop();
|
|
575
|
-
for (const h of installedSignalHandlers)
|
|
576
|
-
process.off(h.sig, h.fn);
|
|
577
|
-
}
|
|
578
|
-
// End-of-run skip summary. Grouped by category so the remediation is
|
|
579
|
-
// targeted to the specific class of failure.
|
|
580
|
-
if (skipReport.skips.length > 0) {
|
|
581
|
-
printSkipSummary(skipReport, resolved.alias);
|
|
582
|
-
}
|
|
583
|
-
// Persist the skip report so --retry-skipped can read it next run.
|
|
584
|
-
// Always written (even when empty) so users can tell whether the last
|
|
585
|
-
// run had skips or just hasn't recorded any yet.
|
|
586
|
-
if (opts.skipReportPath) {
|
|
765
|
+
if (embedQueue) {
|
|
587
766
|
try {
|
|
588
|
-
|
|
589
|
-
const { dirname } = await import("node:path");
|
|
590
|
-
mkdirSync(dirname(opts.skipReportPath), { recursive: true });
|
|
591
|
-
writeFileSync(opts.skipReportPath, JSON.stringify({ recordedAt: Date.now(), orgId: resolved.orgId, ...skipReport }, null, 2), "utf8");
|
|
767
|
+
await embedQueue.drain();
|
|
592
768
|
}
|
|
593
769
|
catch (e) {
|
|
594
|
-
logger.warn("live-ingest:
|
|
770
|
+
logger.warn("live-ingest: embed drain failed", { err: e.message });
|
|
595
771
|
}
|
|
596
772
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
773
|
+
// Full-sync deletion detection: compute previously-known minus touched and
|
|
774
|
+
// delete the difference. Skipped on incremental (SourceMember handles it),
|
|
775
|
+
// skipped if any parse error occurred this run (avoid mass-wipe on
|
|
776
|
+
// transient SF errors).
|
|
777
|
+
if (opts.detectDeletions && mode === "full") {
|
|
778
|
+
if (streamAborted) {
|
|
779
|
+
logger.warn("live-ingest: detect-deletions skipped because bulkRetrieve stream aborted — touchedQnames is incomplete and a full deletion sweep would wrongly wipe untouched-but-still-present nodes");
|
|
780
|
+
}
|
|
781
|
+
else if (parseErrors > 0) {
|
|
782
|
+
logger.warn("live-ingest: detect-deletions skipped due to parseErrors", { parseErrors });
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
try {
|
|
786
|
+
// W1.5-07: per-label drop-ratio guard. The sweep used to run as a
|
|
787
|
+
// single set-difference across ALL labels; a wedged source with
|
|
788
|
+
// streamAborted=false + touchedCount=0 for its label would silently
|
|
789
|
+
// wipe every node of that label (graph-extinction risk that
|
|
790
|
+
// compounds with the W1.5-01/02 wedge cascade). Now we evaluate the
|
|
791
|
+
// guard per label and refuse when the implied drop is suspicious.
|
|
792
|
+
const maxDropRatio = resolveMaxDropRatio(process.env.SFGRAPH_DETECT_DELETIONS_MAX_DROP_RATIO);
|
|
793
|
+
const labels = graph.listAllLabels();
|
|
794
|
+
for (const label of labels) {
|
|
795
|
+
const priorCount = graph.countNodesByLabel(resolved.orgId, label);
|
|
796
|
+
const touchedCount = touchedQnamesByLabel.get(label)?.size ?? 0;
|
|
797
|
+
const verdict = evaluateDeletionGuard(label, priorCount, touchedCount, maxDropRatio);
|
|
798
|
+
if (!verdict.proceed) {
|
|
799
|
+
if (verdict.warning)
|
|
800
|
+
wedgeWarnings.push(verdict.warning);
|
|
801
|
+
logger.warn("live-ingest: detect-deletions refused", {
|
|
802
|
+
label,
|
|
803
|
+
priorCount,
|
|
804
|
+
touchedCount,
|
|
805
|
+
maxDropRatio,
|
|
806
|
+
warning: verdict.warning,
|
|
807
|
+
});
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
// Proceed with deletion for this label only. Scoped to this label's
|
|
811
|
+
// table via listNodesByLabel rather than the global listAllQnames
|
|
812
|
+
// walk, so a refusal for one label cannot accidentally affect
|
|
813
|
+
// sweeps for other labels.
|
|
814
|
+
const touchedSet = touchedQnamesByLabel.get(label);
|
|
815
|
+
const persisted = graph.listNodesByLabel(resolved.orgId, label);
|
|
816
|
+
let labelDeletions = 0;
|
|
817
|
+
for (const node of persisted) {
|
|
818
|
+
const q = String(node.qualifiedName);
|
|
819
|
+
if (touchedSet?.has(q))
|
|
820
|
+
continue;
|
|
821
|
+
graph.deleteEdgesFor(resolved.orgId, asQualifiedName(q));
|
|
822
|
+
graph.deleteNode(resolved.orgId, asQualifiedName(q));
|
|
823
|
+
deletions += 1;
|
|
824
|
+
labelDeletions += 1;
|
|
825
|
+
}
|
|
826
|
+
if (labelDeletions > 0) {
|
|
827
|
+
logger.info("live-ingest: detect-deletions removed stale qnames", {
|
|
828
|
+
label,
|
|
829
|
+
count: labelDeletions,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
}
|
|
624
833
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
graph.deleteNode(resolved.orgId, asQualifiedName(q));
|
|
628
|
-
deletions += 1;
|
|
834
|
+
catch (e) {
|
|
835
|
+
logger.warn("live-ingest: detect-deletions failed", { err: e.message });
|
|
629
836
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// -----------------------------------------------------------------------
|
|
840
|
+
// Post-merge resolver passes. Each step is isolated in try/catch so a
|
|
841
|
+
// single resolver bug never breaks ingest — it just reports zero work done.
|
|
842
|
+
// -----------------------------------------------------------------------
|
|
843
|
+
let crossFlavorEdges = 0;
|
|
844
|
+
let arityResolved = 0;
|
|
845
|
+
let flowMethodsResolved = 0;
|
|
846
|
+
let danglingEdges = 0;
|
|
847
|
+
let overlapResult = { matched: 0, diverged: 0, empty: 0, annotated: 0 };
|
|
848
|
+
let reflectionEdges = 0;
|
|
849
|
+
// Synth a ParseContext for post-passes that need to mint edges/nodes.
|
|
850
|
+
const postCtx = {
|
|
851
|
+
...parseCtxBase,
|
|
852
|
+
sourceUri: "post-merge://resolver",
|
|
853
|
+
};
|
|
854
|
+
if (!opts.disableCrossFlavor) {
|
|
855
|
+
try {
|
|
856
|
+
crossFlavorEdges = resolveCrossFlavor(graph, {
|
|
857
|
+
orgId: resolved.orgId,
|
|
858
|
+
namespace: null,
|
|
859
|
+
ctx: postCtx,
|
|
860
|
+
});
|
|
861
|
+
if (crossFlavorEdges > 0) {
|
|
862
|
+
logger.info("live-ingest: cross-flavor resolver linked Vlocity↔OmniStudio", {
|
|
863
|
+
edges: crossFlavorEdges,
|
|
633
864
|
});
|
|
634
865
|
}
|
|
635
866
|
}
|
|
636
867
|
catch (e) {
|
|
637
|
-
logger.warn("live-ingest:
|
|
868
|
+
logger.warn("live-ingest: cross-flavor resolver failed", { err: e.message });
|
|
638
869
|
}
|
|
639
870
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
let reflectionEdges = 0;
|
|
651
|
-
// Synth a ParseContext for post-passes that need to mint edges/nodes.
|
|
652
|
-
const postCtx = {
|
|
653
|
-
...parseCtxBase,
|
|
654
|
-
sourceUri: "post-merge://resolver",
|
|
655
|
-
};
|
|
656
|
-
if (!opts.disableCrossFlavor) {
|
|
657
|
-
try {
|
|
658
|
-
crossFlavorEdges = resolveCrossFlavor(graph, {
|
|
659
|
-
orgId: resolved.orgId,
|
|
660
|
-
namespace: null,
|
|
661
|
-
ctx: postCtx,
|
|
662
|
-
});
|
|
663
|
-
if (crossFlavorEdges > 0) {
|
|
664
|
-
logger.info("live-ingest: cross-flavor resolver linked Vlocity↔OmniStudio", {
|
|
665
|
-
edges: crossFlavorEdges,
|
|
871
|
+
// Overlap detector: opt-in (off by default) — annotates the CANONICAL_OF
|
|
872
|
+
// edges resolveCrossFlavor just emitted with signaturesMatch + divergence
|
|
873
|
+
// detail. Must run *after* cross-flavor and *before* the audit so the
|
|
874
|
+
// audit sees the annotated edges as the same edges (idempotent merge).
|
|
875
|
+
if (!opts.disableOverlapDetect) {
|
|
876
|
+
try {
|
|
877
|
+
const { detectOmnistudioOverlap } = await import("../parsers/omnistudio/overlap-detector.js");
|
|
878
|
+
overlapResult = detectOmnistudioOverlap(graph, {
|
|
879
|
+
orgId: resolved.orgId,
|
|
880
|
+
ctx: postCtx,
|
|
666
881
|
});
|
|
882
|
+
if (overlapResult.annotated > 0) {
|
|
883
|
+
logger.info("live-ingest: omnistudio overlap detector annotated CANONICAL_OF pairs", {
|
|
884
|
+
matched: overlapResult.matched,
|
|
885
|
+
diverged: overlapResult.diverged,
|
|
886
|
+
empty: overlapResult.empty,
|
|
887
|
+
annotated: overlapResult.annotated,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
667
890
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
// Overlap detector: opt-in (off by default) — annotates the CANONICAL_OF
|
|
674
|
-
// edges resolveCrossFlavor just emitted with signaturesMatch + divergence
|
|
675
|
-
// detail. Must run *after* cross-flavor and *before* the audit so the
|
|
676
|
-
// audit sees the annotated edges as the same edges (idempotent merge).
|
|
677
|
-
if (!opts.disableOverlapDetect) {
|
|
678
|
-
try {
|
|
679
|
-
const { detectOmnistudioOverlap } = await import("../parsers/omnistudio/overlap-detector.js");
|
|
680
|
-
overlapResult = detectOmnistudioOverlap(graph, {
|
|
681
|
-
orgId: resolved.orgId,
|
|
682
|
-
ctx: postCtx,
|
|
683
|
-
});
|
|
684
|
-
if (overlapResult.annotated > 0) {
|
|
685
|
-
logger.info("live-ingest: omnistudio overlap detector annotated CANONICAL_OF pairs", {
|
|
686
|
-
matched: overlapResult.matched,
|
|
687
|
-
diverged: overlapResult.diverged,
|
|
688
|
-
empty: overlapResult.empty,
|
|
689
|
-
annotated: overlapResult.annotated,
|
|
891
|
+
catch (e) {
|
|
892
|
+
logger.warn("live-ingest: omnistudio overlap detector failed", {
|
|
893
|
+
err: e.message,
|
|
690
894
|
});
|
|
691
895
|
}
|
|
692
896
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
897
|
+
if (!opts.disableFlowInvocableResolve) {
|
|
898
|
+
try {
|
|
899
|
+
const flowResult = resolveFlowApexMethods(graph, resolved.orgId, postCtx);
|
|
900
|
+
flowMethodsResolved = flowResult.resolved;
|
|
901
|
+
if (flowResult.scanned > 0) {
|
|
902
|
+
logger.info("live-ingest: flow→apex invocable resolver", {
|
|
903
|
+
scanned: flowResult.scanned,
|
|
904
|
+
resolved: flowResult.resolved,
|
|
905
|
+
missing: flowResult.missing,
|
|
906
|
+
ambiguous: flowResult.ambiguous,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch (e) {
|
|
911
|
+
logger.warn("live-ingest: flow→apex resolver failed", { err: e.message });
|
|
912
|
+
}
|
|
697
913
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (flowResult.scanned > 0) {
|
|
704
|
-
logger.info("live-ingest: flow→apex invocable resolver", {
|
|
705
|
-
scanned: flowResult.scanned,
|
|
706
|
-
resolved: flowResult.resolved,
|
|
707
|
-
missing: flowResult.missing,
|
|
708
|
-
ambiguous: flowResult.ambiguous,
|
|
914
|
+
if (!opts.disableArityResolve) {
|
|
915
|
+
try {
|
|
916
|
+
const arityResult = resolveApexMethodArity(graph, {
|
|
917
|
+
orgId: resolved.orgId,
|
|
918
|
+
ctx: postCtx,
|
|
709
919
|
});
|
|
920
|
+
arityResolved = arityResult.resolved;
|
|
921
|
+
if (arityResult.scanned > 0) {
|
|
922
|
+
logger.info("live-ingest: apex method arity resolver", {
|
|
923
|
+
scanned: arityResult.scanned,
|
|
924
|
+
resolved: arityResult.resolved,
|
|
925
|
+
ambiguous: arityResult.ambiguous,
|
|
926
|
+
unresolved: arityResult.unresolved,
|
|
927
|
+
edgesEmitted: arityResult.edgesEmitted,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
catch (e) {
|
|
932
|
+
logger.warn("live-ingest: apex arity resolver failed", { err: e.message });
|
|
710
933
|
}
|
|
711
934
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
935
|
+
// Reflection walker: runs BEFORE the dangling-edge audit so any
|
|
936
|
+
// REFERENCES edges it emits are validated by the audit alongside
|
|
937
|
+
// parser-emitted edges. Default on; disable via disableReflectionWalker.
|
|
938
|
+
if (!opts.disableReflectionWalker) {
|
|
939
|
+
try {
|
|
940
|
+
const { walkBlobsForReferences } = await import("../parsers/generic/reflection-walker.js");
|
|
941
|
+
const reflectionResult = walkBlobsForReferences(graph, {
|
|
942
|
+
orgId: resolved.orgId,
|
|
943
|
+
ctx: postCtx,
|
|
944
|
+
});
|
|
945
|
+
reflectionEdges = reflectionResult.edgesEmitted;
|
|
946
|
+
if (reflectionEdges > 0) {
|
|
947
|
+
logger.info("live-ingest: reflection walker emitted REFERENCES edges", {
|
|
948
|
+
scanned: reflectionResult.scanned,
|
|
949
|
+
edges: reflectionEdges,
|
|
950
|
+
truncated: reflectionResult.truncatedSources,
|
|
951
|
+
ambiguous: reflectionResult.ambiguousMatches,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
catch (e) {
|
|
956
|
+
logger.warn("live-ingest: reflection walker failed", {
|
|
957
|
+
err: e.message,
|
|
730
958
|
});
|
|
731
959
|
}
|
|
732
960
|
}
|
|
733
|
-
|
|
734
|
-
|
|
961
|
+
if (!opts.disableAudit) {
|
|
962
|
+
try {
|
|
963
|
+
const auditResult = auditDanglingEdges(graph, resolved.orgId, { sampleSize: 25 });
|
|
964
|
+
danglingEdges = auditResult.danglingCount;
|
|
965
|
+
if (auditResult.danglingCount > 0) {
|
|
966
|
+
logger.info("live-ingest: graph audit found dangling edges", {
|
|
967
|
+
totalEdges: auditResult.totalEdges,
|
|
968
|
+
danglingCount: auditResult.danglingCount,
|
|
969
|
+
platformRefs: auditResult.platformRefCount,
|
|
970
|
+
unexpected: auditResult.unexpectedCount,
|
|
971
|
+
byRel: auditResult.byRel,
|
|
972
|
+
byDstPrefix: auditResult.byDstPrefix,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
catch (e) {
|
|
977
|
+
logger.warn("live-ingest: graph audit failed", { err: e.message });
|
|
978
|
+
}
|
|
735
979
|
}
|
|
736
|
-
|
|
737
|
-
// Reflection walker: runs BEFORE the dangling-edge audit so any
|
|
738
|
-
// REFERENCES edges it emits are validated by the audit alongside
|
|
739
|
-
// parser-emitted edges. Default on; disable via disableReflectionWalker.
|
|
740
|
-
if (!opts.disableReflectionWalker) {
|
|
980
|
+
const completedIso = new Date().toISOString();
|
|
741
981
|
try {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
});
|
|
747
|
-
reflectionEdges = reflectionResult.edgesEmitted;
|
|
748
|
-
if (reflectionEdges > 0) {
|
|
749
|
-
logger.info("live-ingest: reflection walker emitted REFERENCES edges", {
|
|
750
|
-
scanned: reflectionResult.scanned,
|
|
751
|
-
edges: reflectionEdges,
|
|
752
|
-
truncated: reflectionResult.truncatedSources,
|
|
753
|
-
ambiguous: reflectionResult.ambiguousMatches,
|
|
754
|
-
});
|
|
755
|
-
}
|
|
982
|
+
// W1.5-08: markSyncComplete subsumes the old touchSync — it updates
|
|
983
|
+
// last_synced_at AND increments sync_generation AND clears the
|
|
984
|
+
// in-progress flag, atomically in one transaction.
|
|
985
|
+
graph.markSyncComplete(resolved.orgId, completedIso);
|
|
756
986
|
}
|
|
757
987
|
catch (e) {
|
|
758
|
-
logger.warn("live-ingest:
|
|
759
|
-
err: e.message,
|
|
760
|
-
});
|
|
988
|
+
logger.warn("live-ingest: markSyncComplete failed", { err: e.message });
|
|
761
989
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
totalEdges: auditResult.totalEdges,
|
|
770
|
-
danglingCount: auditResult.danglingCount,
|
|
771
|
-
byRel: auditResult.byRel,
|
|
772
|
-
byDstPrefix: auditResult.byDstPrefix,
|
|
990
|
+
if (opts.analysisDb) {
|
|
991
|
+
try {
|
|
992
|
+
await populateAnalysisTables(graph, resolved.orgId, opts.analysisDb);
|
|
993
|
+
}
|
|
994
|
+
catch (e) {
|
|
995
|
+
logger.warn("live-ingest: populate analysis tables failed", {
|
|
996
|
+
err: e.message,
|
|
773
997
|
});
|
|
774
998
|
}
|
|
775
999
|
}
|
|
776
|
-
|
|
777
|
-
|
|
1000
|
+
if (opts.snapshotStore) {
|
|
1001
|
+
try {
|
|
1002
|
+
opts.snapshotStore.prune(resolved.orgId, opts.snapshotRetentionDays ?? 30);
|
|
1003
|
+
}
|
|
1004
|
+
catch (e) {
|
|
1005
|
+
logger.warn("live-ingest: snapshot prune failed", { err: e.message });
|
|
1006
|
+
}
|
|
778
1007
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1008
|
+
return {
|
|
1009
|
+
orgId: resolved.orgId,
|
|
1010
|
+
capabilities: caps,
|
|
1011
|
+
mode,
|
|
1012
|
+
membersProcessed,
|
|
1013
|
+
parseErrors,
|
|
1014
|
+
deletions,
|
|
1015
|
+
durationMs: Date.now() - startedAt,
|
|
1016
|
+
crossFlavorEdges,
|
|
1017
|
+
arityResolved,
|
|
1018
|
+
flowMethodsResolved,
|
|
1019
|
+
danglingEdges,
|
|
1020
|
+
reflectionEdges,
|
|
1021
|
+
overlap: overlapResult,
|
|
1022
|
+
warnings: [
|
|
1023
|
+
...skipReport.skips.map((s) => `${s.label}: ${s.reason}`),
|
|
1024
|
+
// Phase 1.5 W1.5-02 namespaced wedge strings. Format documented in
|
|
1025
|
+
// PLAN.md: `wedge:<source>:<stage>:<detail>`. Forward-compatible
|
|
1026
|
+
// with Phase 1's structured-warnings refactor (W1-01).
|
|
1027
|
+
...wedgeWarnings,
|
|
1028
|
+
],
|
|
1029
|
+
};
|
|
783
1030
|
}
|
|
784
1031
|
catch (e) {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1032
|
+
// W1.5-08: ingest threw before reaching markSyncComplete. Flip
|
|
1033
|
+
// sync_in_progress back to 0 (and clear sync_started_at) so concurrent
|
|
1034
|
+
// MCP readers don't see an orphaned in_progress flag forever. Generation
|
|
1035
|
+
// is intentionally NOT incremented — failed ingests don't count as
|
|
1036
|
+
// fresh graph data. Re-throw so callers (CLI / MCP job runner) see the
|
|
1037
|
+
// original error unchanged.
|
|
788
1038
|
try {
|
|
789
|
-
|
|
1039
|
+
graph.markSyncFailed(resolved.orgId);
|
|
790
1040
|
}
|
|
791
|
-
catch (
|
|
792
|
-
logger.warn("live-ingest:
|
|
793
|
-
err:
|
|
1041
|
+
catch (markErr) {
|
|
1042
|
+
logger.warn("live-ingest: markSyncFailed failed", {
|
|
1043
|
+
err: markErr.message,
|
|
794
1044
|
});
|
|
795
1045
|
}
|
|
1046
|
+
throw e;
|
|
796
1047
|
}
|
|
797
|
-
if (opts.snapshotStore) {
|
|
798
|
-
try {
|
|
799
|
-
opts.snapshotStore.prune(resolved.orgId, opts.snapshotRetentionDays ?? 30);
|
|
800
|
-
}
|
|
801
|
-
catch (e) {
|
|
802
|
-
logger.warn("live-ingest: snapshot prune failed", { err: e.message });
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
return {
|
|
806
|
-
orgId: resolved.orgId,
|
|
807
|
-
capabilities: caps,
|
|
808
|
-
mode,
|
|
809
|
-
membersProcessed,
|
|
810
|
-
parseErrors,
|
|
811
|
-
deletions,
|
|
812
|
-
durationMs: Date.now() - startedAt,
|
|
813
|
-
crossFlavorEdges,
|
|
814
|
-
arityResolved,
|
|
815
|
-
flowMethodsResolved,
|
|
816
|
-
danglingEdges,
|
|
817
|
-
reflectionEdges,
|
|
818
|
-
overlap: overlapResult,
|
|
819
|
-
warnings: skipReport.skips.map((s) => `${s.label}: ${s.reason}`),
|
|
820
|
-
};
|
|
821
1048
|
}
|
|
822
1049
|
//# sourceMappingURL=live-ingest.js.map
|