@ryanstark24/sfgraph-core 1.1.8 → 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__/find-nodes.test.d.ts +2 -0
- package/dist/analyze/__tests__/find-nodes.test.d.ts.map +1 -0
- package/dist/analyze/__tests__/find-nodes.test.js +129 -0
- package/dist/analyze/__tests__/find-nodes.test.js.map +1 -0
- package/dist/analyze/__tests__/findings.test.d.ts +2 -0
- package/dist/analyze/__tests__/findings.test.d.ts.map +1 -0
- package/dist/analyze/__tests__/findings.test.js +100 -0
- package/dist/analyze/__tests__/findings.test.js.map +1 -0
- 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 +38 -0
- package/dist/analyze/find-nodes.d.ts.map +1 -0
- package/dist/analyze/find-nodes.js +51 -0
- package/dist/analyze/find-nodes.js.map +1 -0
- package/dist/analyze/findings.d.ts +86 -0
- package/dist/analyze/findings.d.ts.map +1 -0
- package/dist/analyze/findings.js +141 -0
- package/dist/analyze/findings.js.map +1 -0
- package/dist/analyze/index.d.ts +2 -0
- package/dist/analyze/index.d.ts.map +1 -1
- package/dist/analyze/index.js +2 -0
- package/dist/analyze/index.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/edge-fact.d.ts +25 -0
- package/dist/domain/edge-fact.d.ts.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/domain/rel-types.d.ts +4 -0
- package/dist/domain/rel-types.d.ts.map +1 -1
- package/dist/domain/rel-types.js +7 -0
- package/dist/domain/rel-types.js.map +1 -1
- package/dist/embedding/__tests__/embed.test.d.ts +2 -0
- package/dist/embedding/__tests__/embed.test.d.ts.map +1 -0
- package/dist/embedding/__tests__/embed.test.js +72 -0
- package/dist/embedding/__tests__/embed.test.js.map +1 -0
- 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__/liveness.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/liveness.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/liveness.test.js +148 -0
- package/dist/extractors/live-org/__tests__/liveness.test.js.map +1 -0
- package/dist/extractors/live-org/__tests__/mcd-baseline.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/mcd-baseline.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/mcd-baseline.test.js +177 -0
- package/dist/extractors/live-org/__tests__/mcd-baseline.test.js.map +1 -0
- package/dist/extractors/live-org/__tests__/metadata-read-batch.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/metadata-read-batch.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/metadata-read-batch.test.js +75 -0
- package/dist/extractors/live-org/__tests__/metadata-read-batch.test.js.map +1 -0
- package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.js +135 -0
- package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/extractors/live-org/__tests__/soql-rebatch.test.d.ts.map +1 -0
- package/dist/extractors/live-org/__tests__/soql-rebatch.test.js +118 -0
- package/dist/extractors/live-org/__tests__/soql-rebatch.test.js.map +1 -0
- 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__/vlocity-runner.test.js +27 -0
- package/dist/extractors/live-org/__tests__/vlocity-runner.test.js.map +1 -1
- 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 +176 -0
- package/dist/extractors/live-org/bulk-retrieve.d.ts.map +1 -1
- package/dist/extractors/live-org/bulk-retrieve.js +509 -71
- 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 +60 -0
- package/dist/extractors/live-org/extractors/mcd-baseline.d.ts.map +1 -0
- package/dist/extractors/live-org/extractors/mcd-baseline.js +118 -0
- package/dist/extractors/live-org/extractors/mcd-baseline.js.map +1 -0
- 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 +23 -0
- package/dist/extractors/live-org/extractors/omnistudio-retrieve.d.ts.map +1 -0
- package/dist/extractors/live-org/extractors/omnistudio-retrieve.js +172 -0
- package/dist/extractors/live-org/extractors/omnistudio-retrieve.js.map +1 -0
- 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 +98 -15
- package/dist/extractors/live-org/rate-limit.d.ts.map +1 -1
- package/dist/extractors/live-org/rate-limit.js +278 -41
- package/dist/extractors/live-org/rate-limit.js.map +1 -1
- package/dist/extractors/live-org/vlocity/runner.d.ts +1 -1
- package/dist/extractors/live-org/vlocity/runner.d.ts.map +1 -1
- package/dist/extractors/live-org/vlocity/runner.js +49 -14
- package/dist/extractors/live-org/vlocity/runner.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.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__/multi-org-orchestrator.test.js +3 -0
- package/dist/ingest/__tests__/multi-org-orchestrator.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 +86 -0
- package/dist/ingest/live-ingest.d.ts.map +1 -1
- package/dist/ingest/live-ingest.js +783 -449
- 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.d.ts +2 -0
- package/dist/parsers/__tests__/edge-provenance.test.d.ts.map +1 -0
- package/dist/parsers/__tests__/edge-provenance.test.js +47 -0
- package/dist/parsers/__tests__/edge-provenance.test.js.map +1 -0
- package/dist/parsers/__tests__/lwc-bindings.test.js +78 -3
- package/dist/parsers/__tests__/lwc-bindings.test.js.map +1 -1
- package/dist/parsers/__tests__/mcd-gap-fills.test.d.ts +2 -0
- package/dist/parsers/__tests__/mcd-gap-fills.test.d.ts.map +1 -0
- package/dist/parsers/__tests__/mcd-gap-fills.test.js +75 -0
- package/dist/parsers/__tests__/mcd-gap-fills.test.js.map +1 -0
- 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-overlap.test.d.ts +2 -0
- package/dist/parsers/__tests__/omnistudio-overlap.test.d.ts.map +1 -0
- package/dist/parsers/__tests__/omnistudio-overlap.test.js +163 -0
- package/dist/parsers/__tests__/omnistudio-overlap.test.js.map +1 -0
- 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__/reflection-walker.test.d.ts +2 -0
- package/dist/parsers/__tests__/reflection-walker.test.d.ts.map +1 -0
- package/dist/parsers/__tests__/reflection-walker.test.js +162 -0
- package/dist/parsers/__tests__/reflection-walker.test.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/parsers/apex/__tests__/is-test-attribute.test.d.ts.map +1 -0
- package/dist/parsers/apex/__tests__/is-test-attribute.test.js +56 -0
- package/dist/parsers/apex/__tests__/is-test-attribute.test.js.map +1 -0
- package/dist/parsers/apex/__tests__/regex-arg-count.test.d.ts +2 -0
- package/dist/parsers/apex/__tests__/regex-arg-count.test.d.ts.map +1 -0
- package/dist/parsers/apex/__tests__/regex-arg-count.test.js +82 -0
- package/dist/parsers/apex/__tests__/regex-arg-count.test.js.map +1 -0
- 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 +162 -5
- 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/common.d.ts +4 -1
- package/dist/parsers/common.d.ts.map +1 -1
- package/dist/parsers/common.js +15 -2
- package/dist/parsers/common.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 +90 -0
- package/dist/parsers/generic/reflection-walker.d.ts.map +1 -0
- package/dist/parsers/generic/reflection-walker.js +193 -0
- package/dist/parsers/generic/reflection-walker.js.map +1 -0
- package/dist/parsers/lwc/html-visitor.d.ts.map +1 -1
- package/dist/parsers/lwc/html-visitor.js +82 -3
- 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 +35 -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 +50 -0
- package/dist/parsers/omnistudio/overlap-detector.d.ts.map +1 -0
- package/dist/parsers/omnistudio/overlap-detector.js +122 -0
- package/dist/parsers/omnistudio/overlap-detector.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/render/__tests__/sarif.test.d.ts.map +1 -0
- package/dist/render/__tests__/sarif.test.js +126 -0
- package/dist/render/__tests__/sarif.test.js.map +1 -0
- package/dist/render/sarif.d.ts +94 -0
- package/dist/render/sarif.d.ts.map +1 -0
- package/dist/render/sarif.js +131 -0
- package/dist/render/sarif.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/storage/__tests__/rename-stability.test.d.ts.map +1 -0
- package/dist/storage/__tests__/rename-stability.test.js +135 -0
- package/dist/storage/__tests__/rename-stability.test.js.map +1 -0
- 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/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/interfaces.d.ts +60 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/sqlite/graph-store.d.ts +21 -1
- package/dist/storage/sqlite/graph-store.d.ts.map +1 -1
- package/dist/storage/sqlite/graph-store.js +86 -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 +38 -0
- package/dist/storage/sqlite/migrations.js.map +1 -1
- package/dist/storage/sqlite/rename-stability.d.ts +94 -0
- package/dist/storage/sqlite/rename-stability.d.ts.map +1 -0
- package/dist/storage/sqlite/rename-stability.js +92 -0
- package/dist/storage/sqlite/rename-stability.js.map +1 -0
- package/package.json +4 -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,11 +269,52 @@ 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
|
+
};
|
|
182
276
|
}
|
|
183
|
-
default:
|
|
184
|
-
|
|
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
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
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 */
|
|
185
316
|
}
|
|
317
|
+
return { xml: "", ...parsed, name };
|
|
186
318
|
}
|
|
187
319
|
export async function liveIngest(opts) {
|
|
188
320
|
const logger = opts.logger ?? NOOP_LOG;
|
|
@@ -206,510 +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
|
-
const parseCtxBase = {
|
|
247
|
-
orgId: resolved.orgId,
|
|
248
|
-
parseTimestamp: new Date(now).toISOString(),
|
|
249
|
-
namespace: null,
|
|
250
|
-
logger,
|
|
251
|
-
};
|
|
252
|
-
const embedQueue = opts.vectorStore
|
|
253
|
-
? new EmbeddingQueue({
|
|
254
|
-
vectorStore: opts.vectorStore,
|
|
255
|
-
onError: (err) => logger.warn("live-ingest: embedding batch failed", { err: err.message }),
|
|
256
|
-
})
|
|
257
|
-
: null;
|
|
258
|
-
const handleParsed = (parsed) => {
|
|
259
|
-
if (parsed.nodes.length) {
|
|
260
|
-
graph.mergeNodes(parsed.nodes);
|
|
261
|
-
for (const n of parsed.nodes)
|
|
262
|
-
touchedQnames.add(String(n.qualifiedName));
|
|
263
|
-
}
|
|
264
|
-
if (parsed.edges.length)
|
|
265
|
-
graph.mergeEdges(parsed.edges);
|
|
266
|
-
if (parsed.snippets?.length) {
|
|
267
|
-
graph.transaction(() => {
|
|
268
|
-
for (const s of parsed.snippets ?? []) {
|
|
269
|
-
graph.upsertSnippet(s);
|
|
270
|
-
}
|
|
271
|
-
});
|
|
371
|
+
logger.warn("live-ingest: metadata.describe failed", { err: e.message });
|
|
272
372
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
qname: String(n.qualifiedName),
|
|
279
|
-
text,
|
|
280
|
-
orgId: String(n.orgId),
|
|
281
|
-
label: n.label,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
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";
|
|
284
378
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const processOne = async (ref, content) => {
|
|
288
|
-
const qnameForLog = `${ref.memberType}:${ref.memberName}`;
|
|
289
|
-
if (ref.obsolete) {
|
|
290
|
-
// Build the qualified name same way parsers would: best-effort by member name.
|
|
291
|
-
const qname = asQualifiedName(`${ref.memberType}:${ref.memberName}`);
|
|
292
|
-
if (debugProcess)
|
|
293
|
-
console.log(`ingest: [trace] delete ← ${qnameForLog}`);
|
|
294
|
-
graph.deleteEdgesFor(resolved.orgId, qname);
|
|
295
|
-
graph.deleteNode(resolved.orgId, qname);
|
|
296
|
-
if (debugProcess)
|
|
297
|
-
console.log(`ingest: [trace] delete ✓ ${qnameForLog}`);
|
|
298
|
-
deletions += 1;
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
const adapted = adaptParserInput(ref, content);
|
|
302
|
-
if (!adapted)
|
|
303
|
-
return;
|
|
304
|
-
const parser = parserRegistry.for(adapted.type);
|
|
305
|
-
if (!parser)
|
|
306
|
-
return;
|
|
307
|
-
try {
|
|
308
|
-
const ctx = {
|
|
309
|
-
...parseCtxBase,
|
|
310
|
-
sourceUri: ref.sourceUri,
|
|
311
|
-
namespace: ref.namespace ?? null,
|
|
312
|
-
};
|
|
313
|
-
if (debugProcess)
|
|
314
|
-
console.log(`ingest: [trace] parse ← ${qnameForLog}`);
|
|
315
|
-
const result = await parser.parse(adapted.input, ctx);
|
|
316
|
-
if (debugProcess)
|
|
317
|
-
console.log(`ingest: [trace] parse ✓ ${qnameForLog} (nodes=${result.nodes.length} edges=${result.edges.length})`);
|
|
318
|
-
if (debugProcess)
|
|
319
|
-
console.log(`ingest: [trace] graph-merge ← ${qnameForLog}`);
|
|
320
|
-
handleParsed(result);
|
|
321
|
-
if (debugProcess)
|
|
322
|
-
console.log(`ingest: [trace] graph-merge ✓ ${qnameForLog}`);
|
|
323
|
-
membersProcessed += 1;
|
|
379
|
+
else {
|
|
380
|
+
mode = requestedMode;
|
|
324
381
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
err: e.message
|
|
331
|
-
});
|
|
332
|
-
if (debugProcess) {
|
|
333
|
-
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 });
|
|
334
388
|
}
|
|
335
389
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
const { toolingPool, metadataPool, dataPool } = await import("../extractors/live-org/rate-limit.js");
|
|
354
|
-
const tCap = toolingPool._store
|
|
355
|
-
.storeOptions.maxConcurrent;
|
|
356
|
-
const mCap = metadataPool._store
|
|
357
|
-
.storeOptions.maxConcurrent;
|
|
358
|
-
const dCap = dataPool._store
|
|
359
|
-
.storeOptions.maxConcurrent;
|
|
360
|
-
console.log(`ingest: starting full sync (Tooling pool ${tCap} / Metadata pool ${mCap} / Data pool ${dCap} concurrent)`);
|
|
361
|
-
const fanOutStart = Date.now();
|
|
362
|
-
let progressCount = 0;
|
|
363
|
-
let lastTickAt = Date.now();
|
|
364
|
-
let lastActivityAt = Date.now();
|
|
365
|
-
let lastMemberLabel = "(none)";
|
|
366
|
-
const PROGRESS_TICK_MS = 5000; // emit a heartbeat at least every 5s
|
|
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.
|
|
367
407
|
const skipReport = { skips: [] };
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
keepAlive = setInterval(() => { }, 60_000);
|
|
399
|
-
// Background liveness probe. Polls conn.identity() every 30s with a
|
|
400
|
-
// 10s deadline; after two consecutive failures it logs a prominent
|
|
401
|
-
// "CONNECTION LOST" warning so the user knows why surviving
|
|
402
|
-
// extractors are about to time out one by one. We intentionally do
|
|
403
|
-
// NOT abort the ingest from here — per-call 60s timeouts (added in
|
|
404
|
-
// 1.1.3) already cap exposure, and aborting mid-flight would need
|
|
405
|
-
// an AbortSignal threaded through every extractor for marginal
|
|
406
|
-
// benefit. The probe's job is observability, not control flow.
|
|
407
|
-
// Disable via SFGRAPH_NO_LIVENESS_PROBE=1 for hermetic test runs
|
|
408
|
-
// where the mock connection has no identity() implementation.
|
|
409
|
-
if (process.env.SFGRAPH_NO_LIVENESS_PROBE !== "1") {
|
|
410
|
-
livenessProbe = startLivenessProbe(resolved.conn);
|
|
411
|
-
}
|
|
412
|
-
// Wall-clock heartbeat — always on. Without this, silent phases (where
|
|
413
|
-
// all in-window sources are awaiting Bottleneck-queued metadata reads
|
|
414
|
-
// without yielding) look indistinguishable from a hung process. The
|
|
415
|
-
// per-record progress tick at the bottom of the fan-out loop only
|
|
416
|
-
// fires when a record arrives; this fires on a real timer regardless.
|
|
417
|
-
// Debug mode adds heap/rss; baseline shows processed/lastSource/idle.
|
|
418
|
-
const showPoolCounters = debug || process.env.SFGRAPH_DEBUG_POOLS === "1";
|
|
419
|
-
heartbeatTimer = setInterval(() => {
|
|
420
|
-
const idleSec = Math.round((Date.now() - lastActivityAt) / 1000);
|
|
421
|
-
if (idleSec < 10)
|
|
422
|
-
return; // suppress chatter when records are flowing
|
|
423
|
-
if (debug) {
|
|
424
|
-
const mem = process.memoryUsage();
|
|
425
|
-
const heapMb = Math.round(mem.heapUsed / 1024 / 1024);
|
|
426
|
-
const rssMb = Math.round(mem.rss / 1024 / 1024);
|
|
427
|
-
const externalMb = Math.round(mem.external / 1024 / 1024);
|
|
428
|
-
console.log(`ingest: [heartbeat] processed=${progressCount} lastSource=${lastMemberLabel} idle=${idleSec}s heap=${heapMb}MB rss=${rssMb}MB ext=${externalMb}MB`);
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
console.log(`ingest: …still working — processed=${progressCount} idle=${idleSec}s lastSource=${lastMemberLabel}`);
|
|
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);
|
|
432
438
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
["tool", toolingPool],
|
|
441
|
-
["meta", metadataPool],
|
|
442
|
-
["data", dataPool],
|
|
443
|
-
];
|
|
444
|
-
for (const [name, p] of snapshot) {
|
|
445
|
-
const c = p.counts();
|
|
446
|
-
p.currentReservoir().then((reservoir) => {
|
|
447
|
-
console.log(`ingest: [pool ${name}] running=${c.RUNNING} executing=${c.EXECUTING} queued=${c.QUEUED} received=${c.RECEIVED} reservoir=${reservoir}`);
|
|
448
|
-
}, () => {
|
|
449
|
-
/* swallow — never let a diag failure kill ingest */
|
|
450
|
-
});
|
|
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);
|
|
451
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
|
+
});
|
|
452
457
|
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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;
|
|
481
|
+
try {
|
|
482
|
+
const ctx = {
|
|
483
|
+
...parseCtxBase,
|
|
484
|
+
sourceUri: ref.sourceUri,
|
|
485
|
+
namespace: ref.namespace ?? null,
|
|
466
486
|
};
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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;
|
|
472
498
|
}
|
|
499
|
+
catch (e) {
|
|
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}`);
|
|
508
|
+
}
|
|
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);
|
|
560
|
+
}
|
|
561
|
+
logger.info("live-ingest: MCD baseline complete", {
|
|
562
|
+
edges: mcd.edges.length,
|
|
563
|
+
byType: mcd.byType,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
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(", ")}`);
|
|
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.)
|
|
473
599
|
try {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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`);
|
|
480
637
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
logger.warn("live-ingest: processOne failed", {
|
|
484
|
-
qname: `${member.ref.memberType}:${member.ref.memberName}`,
|
|
485
|
-
error: e.message,
|
|
486
|
-
});
|
|
638
|
+
else {
|
|
639
|
+
console.log(`ingest: …still working — processed=${progressCount} idle=${idleSec}s lastSource=${lastMemberLabel}`);
|
|
487
640
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
}
|
|
498
660
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
+
}
|
|
503
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
|
+
});
|
|
504
734
|
}
|
|
505
|
-
console.log(`ingest: fan-out complete (${progressCount} members in ${Math.round((Date.now() - fanOutStart) / 1000)}s)`);
|
|
506
|
-
const gs = graph;
|
|
507
|
-
if (typeof gs.checkpoint === "function")
|
|
508
|
-
gs.checkpoint();
|
|
509
735
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
+
}
|
|
517
763
|
}
|
|
518
764
|
}
|
|
519
|
-
|
|
520
|
-
if (heartbeatTimer)
|
|
521
|
-
clearInterval(heartbeatTimer);
|
|
522
|
-
if (keepAlive)
|
|
523
|
-
clearInterval(keepAlive);
|
|
524
|
-
livenessProbe?.stop();
|
|
525
|
-
for (const h of installedSignalHandlers)
|
|
526
|
-
process.off(h.sig, h.fn);
|
|
527
|
-
}
|
|
528
|
-
// End-of-run skip summary. Grouped by category so the remediation is
|
|
529
|
-
// targeted to the specific class of failure.
|
|
530
|
-
if (skipReport.skips.length > 0) {
|
|
531
|
-
printSkipSummary(skipReport, resolved.alias);
|
|
532
|
-
}
|
|
533
|
-
// Persist the skip report so --retry-skipped can read it next run.
|
|
534
|
-
// Always written (even when empty) so users can tell whether the last
|
|
535
|
-
// run had skips or just hasn't recorded any yet.
|
|
536
|
-
if (opts.skipReportPath) {
|
|
765
|
+
if (embedQueue) {
|
|
537
766
|
try {
|
|
538
|
-
|
|
539
|
-
const { dirname } = await import("node:path");
|
|
540
|
-
mkdirSync(dirname(opts.skipReportPath), { recursive: true });
|
|
541
|
-
writeFileSync(opts.skipReportPath, JSON.stringify({ recordedAt: Date.now(), orgId: resolved.orgId, ...skipReport }, null, 2), "utf8");
|
|
767
|
+
await embedQueue.drain();
|
|
542
768
|
}
|
|
543
769
|
catch (e) {
|
|
544
|
-
logger.warn("live-ingest:
|
|
770
|
+
logger.warn("live-ingest: embed drain failed", { err: e.message });
|
|
545
771
|
}
|
|
546
772
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch (e) {
|
|
835
|
+
logger.warn("live-ingest: detect-deletions failed", { err: e.message });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
566
838
|
}
|
|
567
|
-
|
|
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) {
|
|
568
855
|
try {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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,
|
|
864
|
+
});
|
|
574
865
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
866
|
+
}
|
|
867
|
+
catch (e) {
|
|
868
|
+
logger.warn("live-ingest: cross-flavor resolver failed", { err: e.message });
|
|
869
|
+
}
|
|
870
|
+
}
|
|
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,
|
|
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
|
+
});
|
|
579
889
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
890
|
+
}
|
|
891
|
+
catch (e) {
|
|
892
|
+
logger.warn("live-ingest: omnistudio overlap detector failed", {
|
|
893
|
+
err: e.message,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
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,
|
|
583
907
|
});
|
|
584
908
|
}
|
|
585
909
|
}
|
|
586
910
|
catch (e) {
|
|
587
|
-
logger.warn("live-ingest:
|
|
911
|
+
logger.warn("live-ingest: flow→apex resolver failed", { err: e.message });
|
|
588
912
|
}
|
|
589
913
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
let crossFlavorEdges = 0;
|
|
596
|
-
let arityResolved = 0;
|
|
597
|
-
let flowMethodsResolved = 0;
|
|
598
|
-
let danglingEdges = 0;
|
|
599
|
-
// Synth a ParseContext for post-passes that need to mint edges/nodes.
|
|
600
|
-
const postCtx = {
|
|
601
|
-
...parseCtxBase,
|
|
602
|
-
sourceUri: "post-merge://resolver",
|
|
603
|
-
};
|
|
604
|
-
if (!opts.disableCrossFlavor) {
|
|
605
|
-
try {
|
|
606
|
-
crossFlavorEdges = resolveCrossFlavor(graph, {
|
|
607
|
-
orgId: resolved.orgId,
|
|
608
|
-
namespace: null,
|
|
609
|
-
ctx: postCtx,
|
|
610
|
-
});
|
|
611
|
-
if (crossFlavorEdges > 0) {
|
|
612
|
-
logger.info("live-ingest: cross-flavor resolver linked Vlocity↔OmniStudio", {
|
|
613
|
-
edges: crossFlavorEdges,
|
|
914
|
+
if (!opts.disableArityResolve) {
|
|
915
|
+
try {
|
|
916
|
+
const arityResult = resolveApexMethodArity(graph, {
|
|
917
|
+
orgId: resolved.orgId,
|
|
918
|
+
ctx: postCtx,
|
|
614
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 });
|
|
615
933
|
}
|
|
616
934
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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,
|
|
631
958
|
});
|
|
632
959
|
}
|
|
633
960
|
}
|
|
634
|
-
|
|
635
|
-
|
|
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
|
+
}
|
|
636
979
|
}
|
|
637
|
-
|
|
638
|
-
if (!opts.disableArityResolve) {
|
|
980
|
+
const completedIso = new Date().toISOString();
|
|
639
981
|
try {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
arityResolved = arityResult.resolved;
|
|
645
|
-
if (arityResult.scanned > 0) {
|
|
646
|
-
logger.info("live-ingest: apex method arity resolver", {
|
|
647
|
-
scanned: arityResult.scanned,
|
|
648
|
-
resolved: arityResult.resolved,
|
|
649
|
-
ambiguous: arityResult.ambiguous,
|
|
650
|
-
unresolved: arityResult.unresolved,
|
|
651
|
-
edgesEmitted: arityResult.edgesEmitted,
|
|
652
|
-
});
|
|
653
|
-
}
|
|
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);
|
|
654
986
|
}
|
|
655
987
|
catch (e) {
|
|
656
|
-
logger.warn("live-ingest:
|
|
988
|
+
logger.warn("live-ingest: markSyncComplete failed", { err: e.message });
|
|
657
989
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
totalEdges: auditResult.totalEdges,
|
|
666
|
-
danglingCount: auditResult.danglingCount,
|
|
667
|
-
byRel: auditResult.byRel,
|
|
668
|
-
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,
|
|
669
997
|
});
|
|
670
998
|
}
|
|
671
999
|
}
|
|
672
|
-
|
|
673
|
-
|
|
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
|
+
}
|
|
674
1007
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
+
};
|
|
679
1030
|
}
|
|
680
1031
|
catch (e) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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.
|
|
684
1038
|
try {
|
|
685
|
-
|
|
1039
|
+
graph.markSyncFailed(resolved.orgId);
|
|
686
1040
|
}
|
|
687
|
-
catch (
|
|
688
|
-
logger.warn("live-ingest:
|
|
689
|
-
err:
|
|
1041
|
+
catch (markErr) {
|
|
1042
|
+
logger.warn("live-ingest: markSyncFailed failed", {
|
|
1043
|
+
err: markErr.message,
|
|
690
1044
|
});
|
|
691
1045
|
}
|
|
1046
|
+
throw e;
|
|
692
1047
|
}
|
|
693
|
-
if (opts.snapshotStore) {
|
|
694
|
-
try {
|
|
695
|
-
opts.snapshotStore.prune(resolved.orgId, opts.snapshotRetentionDays ?? 30);
|
|
696
|
-
}
|
|
697
|
-
catch (e) {
|
|
698
|
-
logger.warn("live-ingest: snapshot prune failed", { err: e.message });
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
return {
|
|
702
|
-
orgId: resolved.orgId,
|
|
703
|
-
capabilities: caps,
|
|
704
|
-
mode,
|
|
705
|
-
membersProcessed,
|
|
706
|
-
parseErrors,
|
|
707
|
-
deletions,
|
|
708
|
-
durationMs: Date.now() - startedAt,
|
|
709
|
-
crossFlavorEdges,
|
|
710
|
-
arityResolved,
|
|
711
|
-
flowMethodsResolved,
|
|
712
|
-
danglingEdges,
|
|
713
|
-
};
|
|
714
1048
|
}
|
|
715
1049
|
//# sourceMappingURL=live-ingest.js.map
|