gitnexus 1.6.10-rc.98 → 1.6.10
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/README.md +177 -18
- package/dist/_shared/graph/types.d.ts +21 -2
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +3 -3
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +1 -1
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/integrations/circuit-breaker.d.ts.map +1 -1
- package/dist/_shared/integrations/circuit-breaker.js.map +1 -1
- package/dist/_shared/integrations/resilient-fetch.d.ts +16 -0
- package/dist/_shared/integrations/resilient-fetch.d.ts.map +1 -1
- package/dist/_shared/integrations/resilient-fetch.js +19 -7
- package/dist/_shared/integrations/resilient-fetch.js.map +1 -1
- package/dist/_shared/language-detection.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.d.ts +2 -2
- package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
- package/dist/_shared/lbug/schema-constants.js +3 -0
- package/dist/_shared/lbug/schema-constants.js.map +1 -1
- package/dist/_shared/scope-resolution/evidence-weights.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/finalize-algorithm.d.ts +5 -0
- package/dist/_shared/scope-resolution/finalize-algorithm.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/finalize-algorithm.js +315 -36
- package/dist/_shared/scope-resolution/finalize-algorithm.js.map +1 -1
- package/dist/_shared/scope-resolution/reference-site.d.ts +115 -0
- package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -1
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/registries/lookup-core.js +58 -2
- package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -1
- package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -1
- package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +47 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/types.d.ts +278 -3
- package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
- package/dist/cli/ai-context.d.ts +3 -2
- package/dist/cli/ai-context.js +71 -16
- package/dist/cli/analyze-config.d.ts +1 -1
- package/dist/cli/analyze-options.d.ts +131 -0
- package/dist/cli/analyze-options.js +1 -0
- package/dist/cli/analyze.d.ts +21 -113
- package/dist/cli/analyze.js +172 -29
- package/dist/cli/cli-message.d.ts +1 -1
- package/dist/cli/detect-changes-format.js +19 -2
- package/dist/cli/doctor.d.ts +12 -0
- package/dist/cli/doctor.js +32 -5
- package/dist/cli/editor-targets.js +6 -0
- package/dist/cli/eval-server.js +2 -1
- package/dist/cli/format-symbol.d.ts +15 -0
- package/dist/cli/format-symbol.js +17 -0
- package/dist/cli/generated-skill.d.ts +17 -0
- package/dist/cli/generated-skill.js +1 -0
- package/dist/cli/group.js +229 -21
- package/dist/cli/help-i18n.js +1 -2
- package/dist/cli/i18n/en.d.ts +11 -9
- package/dist/cli/i18n/en.js +13 -9
- package/dist/cli/i18n/resources.d.ts +15 -11
- package/dist/cli/i18n/zh-CN.d.ts +4 -2
- package/dist/cli/i18n/zh-CN.js +11 -9
- package/dist/cli/index.js +8 -4
- package/dist/cli/resolve-invocation.d.ts +7 -3
- package/dist/cli/resolve-invocation.js +7 -3
- package/dist/cli/setup.js +4 -3
- package/dist/cli/skill-gen.d.ts +1 -6
- package/dist/cli/tool.d.ts +1 -0
- package/dist/cli/tool.js +74 -17
- package/dist/cli/wiki.js +53 -10
- package/dist/config/ignore-service.d.ts +2 -0
- package/dist/config/ignore-service.js +54 -13
- package/dist/core/analyzer-identity.d.ts +80 -0
- package/dist/core/analyzer-identity.js +364 -26
- package/dist/core/augmentation/engine.js +86 -71
- package/dist/core/embedding-checkpoint.d.ts +143 -0
- package/dist/core/embedding-checkpoint.js +221 -0
- package/dist/core/embedding-count.d.ts +61 -0
- package/dist/core/embedding-count.js +69 -0
- package/dist/core/embeddings/ast-utils.js +9 -1
- package/dist/core/embeddings/embedding-pipeline.d.ts +8 -0
- package/dist/core/embeddings/embedding-pipeline.js +253 -14
- package/dist/core/embeddings/http-client.d.ts +5 -0
- package/dist/core/embeddings/http-client.js +142 -27
- package/dist/core/graph/graph.js +3 -0
- package/dist/core/graph/import-cycles.d.ts +194 -3
- package/dist/core/graph/import-cycles.js +522 -73
- package/dist/core/graph/types.d.ts +18 -0
- package/dist/core/group/bridge-db.d.ts +204 -1
- package/dist/core/group/bridge-db.js +401 -28
- package/dist/core/group/completeness.d.ts +130 -0
- package/dist/core/group/completeness.js +83 -0
- package/dist/core/group/config-parser.js +0 -5
- package/dist/core/group/cross-impact.d.ts +46 -1
- package/dist/core/group/cross-impact.js +220 -19
- package/dist/core/group/cross-trace.d.ts +32 -11
- package/dist/core/group/cross-trace.js +114 -20
- package/dist/core/group/extractors/http-patterns/java.js +278 -22
- package/dist/core/group/extractors/http-patterns/node.js +332 -216
- package/dist/core/group/extractors/http-patterns/types.d.ts +8 -0
- package/dist/core/group/extractors/http-route-extractor.d.ts +1 -0
- package/dist/core/group/extractors/http-route-extractor.js +122 -48
- package/dist/core/group/extractors/include-extractor.js +20 -2
- package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
- package/dist/core/group/extractors/manifest-extractor.js +7 -7
- package/dist/core/group/extractors/python-workspace-extractor.js +5 -2
- package/dist/core/group/group-lock.d.ts +31 -0
- package/dist/core/group/group-lock.js +166 -0
- package/dist/core/group/service.js +297 -16
- package/dist/core/group/storage.d.ts +3 -0
- package/dist/core/group/storage.js +7 -39
- package/dist/core/group/sync.d.ts +49 -3
- package/dist/core/group/sync.js +0 -0
- package/dist/core/group/types.d.ts +183 -8
- package/dist/core/group/types.js +28 -1
- package/dist/core/incremental/subgraph-extract.d.ts +9 -6
- package/dist/core/incremental/subgraph-extract.js +33 -10
- package/dist/core/index-freshness.d.ts +77 -2
- package/dist/core/index-freshness.js +109 -2
- package/dist/core/ingestion/call-processor.d.ts +13 -1
- package/dist/core/ingestion/call-processor.js +110 -3
- package/dist/core/ingestion/cfg/callee-cell-format.d.ts +42 -0
- package/dist/core/ingestion/cfg/callee-cell-format.js +42 -0
- package/dist/core/ingestion/cfg/emit.d.ts +8 -23
- package/dist/core/ingestion/cfg/emit.js +9 -23
- package/dist/core/ingestion/class-extractors/configs/go.js +10 -5
- package/dist/core/ingestion/class-extractors/configs/jvm.js +6 -6
- package/dist/core/ingestion/cluster-enricher.js +8 -4
- package/dist/core/ingestion/community-processor.d.ts +21 -3
- package/dist/core/ingestion/community-processor.js +81 -82
- package/dist/core/ingestion/di-extractors/index.d.ts +9 -33
- package/dist/core/ingestion/di-extractors/spring.d.ts +1 -1
- package/dist/core/ingestion/di-extractors/spring.js +8 -1
- package/dist/core/ingestion/di-extractors/types.d.ts +60 -0
- package/dist/core/ingestion/di-extractors/types.js +15 -0
- package/dist/core/ingestion/filesystem-walker.js +30 -14
- package/dist/core/ingestion/finalize-orchestrator.js +1 -0
- package/dist/core/ingestion/frameworks/spring/analysis-features.d.ts +6 -0
- package/dist/core/ingestion/frameworks/spring/analysis-features.js +35 -1
- package/dist/core/ingestion/frameworks/spring/annotation-arguments.d.ts +13 -0
- package/dist/core/ingestion/frameworks/spring/annotation-arguments.js +227 -0
- package/dist/core/ingestion/frameworks/spring/aop-candidates.d.ts +15 -0
- package/dist/core/ingestion/frameworks/spring/aop-candidates.js +85 -0
- package/dist/core/ingestion/frameworks/spring/aop.d.ts +118 -0
- package/dist/core/ingestion/frameworks/spring/aop.js +476 -0
- package/dist/core/ingestion/frameworks/spring/auto-configuration.d.ts +8 -0
- package/dist/core/ingestion/frameworks/spring/auto-configuration.js +16 -0
- package/dist/core/ingestion/frameworks/spring/bean-catalog.js +1 -0
- package/dist/core/ingestion/frameworks/spring/bean-factories.d.ts +33 -0
- package/dist/core/ingestion/frameworks/spring/bean-factories.js +65 -0
- package/dist/core/ingestion/frameworks/spring/conditionals.d.ts +25 -0
- package/dist/core/ingestion/frameworks/spring/conditionals.js +319 -0
- package/dist/core/ingestion/frameworks/spring/di-metadata.d.ts +3 -0
- package/dist/core/ingestion/frameworks/spring/di-metadata.js +162 -13
- package/dist/core/ingestion/frameworks/spring/non-http-handlers.d.ts +28 -0
- package/dist/core/ingestion/frameworks/spring/non-http-handlers.js +152 -0
- package/dist/core/ingestion/frameworks/spring/resource-injection.d.ts +5 -0
- package/dist/core/ingestion/frameworks/spring/resource-injection.js +91 -0
- package/dist/core/ingestion/import-resolvers/configs/csharp.js +5 -1
- package/dist/core/ingestion/import-resolvers/configs/swift.js +13 -0
- package/dist/core/ingestion/import-resolvers/csharp.d.ts +9 -1
- package/dist/core/ingestion/import-resolvers/csharp.js +234 -27
- package/dist/core/ingestion/import-resolvers/go.d.ts +11 -1
- package/dist/core/ingestion/import-resolvers/go.js +44 -17
- package/dist/core/ingestion/import-resolvers/jvm.d.ts +2 -2
- package/dist/core/ingestion/import-resolvers/node-workspace-packages.d.ts +90 -0
- package/dist/core/ingestion/import-resolvers/node-workspace-packages.js +429 -0
- package/dist/core/ingestion/import-resolvers/package-dir-index.d.ts +124 -0
- package/dist/core/ingestion/import-resolvers/package-dir-index.js +223 -0
- package/dist/core/ingestion/import-resolvers/pass-cache.d.ts +50 -0
- package/dist/core/ingestion/import-resolvers/pass-cache.js +60 -0
- package/dist/core/ingestion/import-resolvers/per-file-set.d.ts +71 -0
- package/dist/core/ingestion/import-resolvers/per-file-set.js +11 -0
- package/dist/core/ingestion/import-resolvers/php.d.ts +1 -1
- package/dist/core/ingestion/import-resolvers/php.js +65 -19
- package/dist/core/ingestion/import-resolvers/python-file-index.d.ts +201 -0
- package/dist/core/ingestion/import-resolvers/python-file-index.js +302 -0
- package/dist/core/ingestion/import-resolvers/python.js +25 -6
- package/dist/core/ingestion/import-resolvers/ruby.d.ts +1 -1
- package/dist/core/ingestion/import-resolvers/rust.d.ts +2 -2
- package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
- package/dist/core/ingestion/import-resolvers/types.d.ts +1 -12
- package/dist/core/ingestion/import-resolvers/utils.d.ts +65 -6
- package/dist/core/ingestion/import-resolvers/utils.js +215 -36
- package/dist/core/ingestion/import-resolvers/workspace-file-index.d.ts +72 -0
- package/dist/core/ingestion/import-resolvers/workspace-file-index.js +58 -0
- package/dist/core/ingestion/language-config.d.ts +29 -1
- package/dist/core/ingestion/language-config.js +110 -35
- package/dist/core/ingestion/language-provider.d.ts +209 -7
- package/dist/core/ingestion/language-provider.js +37 -0
- package/dist/core/ingestion/languages/c/captures.js +7 -0
- package/dist/core/ingestion/languages/c/import-target.js +12 -16
- package/dist/core/ingestion/languages/c/scope-resolver.js +20 -19
- package/dist/core/ingestion/languages/c/static-linkage.js +12 -16
- package/dist/core/ingestion/languages/c-cpp.js +13 -0
- package/dist/core/ingestion/languages/cobol/scope-resolver.js +27 -20
- package/dist/core/ingestion/languages/cobol.js +13 -0
- package/dist/core/ingestion/languages/cpp/adl.d.ts +2 -2
- package/dist/core/ingestion/languages/cpp/adl.js +67 -44
- package/dist/core/ingestion/languages/cpp/captures.js +14 -2
- package/dist/core/ingestion/languages/cpp/file-local-linkage.js +11 -15
- package/dist/core/ingestion/languages/cpp/inline-namespaces.d.ts +28 -8
- package/dist/core/ingestion/languages/cpp/inline-namespaces.js +242 -68
- package/dist/core/ingestion/languages/cpp/interpret.d.ts +2 -0
- package/dist/core/ingestion/languages/cpp/interpret.js +126 -6
- package/dist/core/ingestion/languages/cpp/query.js +106 -0
- package/dist/core/ingestion/languages/cpp/scope-resolver.js +48 -20
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +2 -15
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +18 -10
- package/dist/core/ingestion/languages/csharp/captures.js +19 -0
- package/dist/core/ingestion/languages/csharp/import-target.js +46 -72
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +5 -1
- package/dist/core/ingestion/languages/csharp/query.js +25 -5
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +75 -2
- package/dist/core/ingestion/languages/dart/captures.d.ts +10 -0
- package/dist/core/ingestion/languages/dart/captures.js +598 -16
- package/dist/core/ingestion/languages/dart/import-target.js +32 -9
- package/dist/core/ingestion/languages/dart/index.d.ts +1 -1
- package/dist/core/ingestion/languages/dart/index.js +1 -1
- package/dist/core/ingestion/languages/dart/query.js +98 -1
- package/dist/core/ingestion/languages/dart/scope-resolver.js +14 -3
- package/dist/core/ingestion/languages/dart/simple-hooks.js +9 -0
- package/dist/core/ingestion/languages/dart.js +6 -1
- package/dist/core/ingestion/languages/go/captures.js +108 -3
- package/dist/core/ingestion/languages/go/generic-type-parameters.d.ts +31 -0
- package/dist/core/ingestion/languages/go/generic-type-parameters.js +140 -0
- package/dist/core/ingestion/languages/go/import-target.d.ts +3 -10
- package/dist/core/ingestion/languages/go/import-target.js +34 -30
- package/dist/core/ingestion/languages/go/index.d.ts +1 -1
- package/dist/core/ingestion/languages/go/interface-impls.d.ts +9 -1
- package/dist/core/ingestion/languages/go/interface-impls.js +693 -87
- package/dist/core/ingestion/languages/go/interpret.d.ts +2 -0
- package/dist/core/ingestion/languages/go/interpret.js +22 -8
- package/dist/core/ingestion/languages/go/method-owners.js +31 -12
- package/dist/core/ingestion/languages/go/package-clause.d.ts +46 -0
- package/dist/core/ingestion/languages/go/package-clause.js +75 -0
- package/dist/core/ingestion/languages/go/package-siblings.js +7 -11
- package/dist/core/ingestion/languages/go/query.js +56 -3
- package/dist/core/ingestion/languages/go/scope-resolver.js +39 -0
- package/dist/core/ingestion/languages/go.js +9 -0
- package/dist/core/ingestion/languages/java/analysis-features.d.ts +4 -0
- package/dist/core/ingestion/languages/java/analysis-features.js +12 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +12 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +48 -0
- package/dist/core/ingestion/languages/java/captures.js +129 -44
- package/dist/core/ingestion/languages/java/import-target.d.ts +49 -9
- package/dist/core/ingestion/languages/java/import-target.js +68 -88
- package/dist/core/ingestion/languages/java/interpret.js +3 -2
- package/dist/core/ingestion/languages/java/module-resolution.d.ts +62 -0
- package/dist/core/ingestion/languages/java/module-resolution.js +142 -0
- package/dist/core/ingestion/languages/java/query.js +16 -3
- package/dist/core/ingestion/languages/java/record-components.d.ts +14 -0
- package/dist/core/ingestion/languages/java/record-components.js +185 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +10 -2
- package/dist/core/ingestion/languages/java/spring-aop.d.ts +12 -0
- package/dist/core/ingestion/languages/java/spring-aop.js +49 -0
- package/dist/core/ingestion/languages/java/spring-conditionals.d.ts +11 -0
- package/dist/core/ingestion/languages/java/spring-conditionals.js +48 -0
- package/dist/core/ingestion/languages/java/spring-di.d.ts +4 -1
- package/dist/core/ingestion/languages/java/spring-di.js +41 -9
- package/dist/core/ingestion/languages/java/spring-non-http-handlers.d.ts +7 -0
- package/dist/core/ingestion/languages/java/spring-non-http-handlers.js +32 -0
- package/dist/core/ingestion/languages/java.js +94 -3
- package/dist/core/ingestion/languages/javascript/captures.d.ts +6 -1
- package/dist/core/ingestion/languages/javascript/captures.js +262 -4
- package/dist/core/ingestion/languages/javascript/import-target.d.ts +17 -21
- package/dist/core/ingestion/languages/javascript/import-target.js +24 -41
- package/dist/core/ingestion/languages/javascript/index.d.ts +54 -5
- package/dist/core/ingestion/languages/javascript/index.js +54 -5
- package/dist/core/ingestion/languages/javascript/query.d.ts +1 -0
- package/dist/core/ingestion/languages/javascript/query.js +211 -5
- package/dist/core/ingestion/languages/javascript/scope-resolver.js +12 -0
- package/dist/core/ingestion/languages/javascript/simple-hooks.js +2 -1
- package/dist/core/ingestion/languages/jvm/package-siblings.js +77 -9
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +15 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +48 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +43 -1
- package/dist/core/ingestion/languages/kotlin/import-target.d.ts +8 -1
- package/dist/core/ingestion/languages/kotlin/import-target.js +20 -123
- package/dist/core/ingestion/languages/kotlin/index.d.ts +1 -0
- package/dist/core/ingestion/languages/kotlin/index.js +1 -0
- package/dist/core/ingestion/languages/kotlin/module-resolution.d.ts +22 -0
- package/dist/core/ingestion/languages/kotlin/module-resolution.js +108 -0
- package/dist/core/ingestion/languages/kotlin/query.js +28 -2
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +23 -2
- package/dist/core/ingestion/languages/kotlin/spring-aop.d.ts +12 -0
- package/dist/core/ingestion/languages/kotlin/spring-aop.js +64 -0
- package/dist/core/ingestion/languages/kotlin/spring-conditionals.d.ts +12 -0
- package/dist/core/ingestion/languages/kotlin/spring-conditionals.js +49 -0
- package/dist/core/ingestion/languages/kotlin/spring-di.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-di.js +60 -11
- package/dist/core/ingestion/languages/kotlin/spring-non-http-handlers.d.ts +10 -0
- package/dist/core/ingestion/languages/kotlin/spring-non-http-handlers.js +41 -0
- package/dist/core/ingestion/languages/php/captures.d.ts +5 -0
- package/dist/core/ingestion/languages/php/captures.js +319 -51
- package/dist/core/ingestion/languages/php/import-target.d.ts +3 -4
- package/dist/core/ingestion/languages/php/import-target.js +192 -62
- package/dist/core/ingestion/languages/php/query.js +31 -0
- package/dist/core/ingestion/languages/python/arity-metadata.d.ts +1 -0
- package/dist/core/ingestion/languages/python/arity-metadata.js +1 -0
- package/dist/core/ingestion/languages/python/captures.js +29 -0
- package/dist/core/ingestion/languages/python/import-decomposer.js +22 -1
- package/dist/core/ingestion/languages/python/import-target.d.ts +42 -1
- package/dist/core/ingestion/languages/python/import-target.js +277 -91
- package/dist/core/ingestion/languages/python/index.d.ts +1 -1
- package/dist/core/ingestion/languages/python/index.js +1 -1
- package/dist/core/ingestion/languages/python/interpret.d.ts +52 -0
- package/dist/core/ingestion/languages/python/interpret.js +249 -7
- package/dist/core/ingestion/languages/python/query.js +9 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +8 -6
- package/dist/core/ingestion/languages/python/receiver-binding.js +108 -12
- package/dist/core/ingestion/languages/python/scope-resolver.js +27 -4
- package/dist/core/ingestion/languages/python/simple-hooks.js +2 -8
- package/dist/core/ingestion/languages/python.js +13 -0
- package/dist/core/ingestion/languages/ruby/captures.js +158 -0
- package/dist/core/ingestion/languages/ruby/import-target.js +7 -5
- package/dist/core/ingestion/languages/ruby/query.js +93 -0
- package/dist/core/ingestion/languages/ruby/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/ruby/simple-hooks.d.ts +1 -1
- package/dist/core/ingestion/languages/ruby/simple-hooks.js +22 -1
- package/dist/core/ingestion/languages/rust/captures.js +39 -1
- package/dist/core/ingestion/languages/rust/method-owners.js +11 -1
- package/dist/core/ingestion/languages/rust/module-path.d.ts +126 -0
- package/dist/core/ingestion/languages/rust/module-path.js +214 -0
- package/dist/core/ingestion/languages/rust/qualified-call.d.ts +41 -0
- package/dist/core/ingestion/languages/rust/qualified-call.js +481 -0
- package/dist/core/ingestion/languages/rust/query.js +39 -6
- package/dist/core/ingestion/languages/rust/scope-resolver.js +21 -2
- package/dist/core/ingestion/languages/rust.js +17 -0
- package/dist/core/ingestion/languages/swift/captures.js +87 -3
- package/dist/core/ingestion/languages/swift/conditional-directive-preprocess.d.ts +48 -0
- package/dist/core/ingestion/languages/swift/conditional-directive-preprocess.js +253 -0
- package/dist/core/ingestion/languages/swift/import-target.js +4 -9
- package/dist/core/ingestion/languages/swift/query.js +17 -0
- package/dist/core/ingestion/languages/swift/receiver-binding.d.ts +8 -0
- package/dist/core/ingestion/languages/swift/receiver-binding.js +6 -2
- package/dist/core/ingestion/languages/swift.js +2 -0
- package/dist/core/ingestion/languages/typescript/captures.d.ts +90 -1
- package/dist/core/ingestion/languages/typescript/captures.js +267 -5
- package/dist/core/ingestion/languages/typescript/cjs-export-assignment.d.ts +134 -0
- package/dist/core/ingestion/languages/typescript/cjs-export-assignment.js +523 -0
- package/dist/core/ingestion/languages/typescript/cjs-module-exports.d.ts +54 -0
- package/dist/core/ingestion/languages/typescript/cjs-module-exports.js +191 -0
- package/dist/core/ingestion/languages/typescript/convex-endpoint-metadata.d.ts +7 -0
- package/dist/core/ingestion/languages/typescript/convex-endpoint-metadata.js +92 -0
- package/dist/core/ingestion/languages/typescript/file-candidates.d.ts +29 -0
- package/dist/core/ingestion/languages/typescript/file-candidates.js +70 -0
- package/dist/core/ingestion/languages/typescript/import-decomposer.d.ts +28 -1
- package/dist/core/ingestion/languages/typescript/import-decomposer.js +125 -10
- package/dist/core/ingestion/languages/typescript/import-target.d.ts +25 -37
- package/dist/core/ingestion/languages/typescript/import-target.js +24 -26
- package/dist/core/ingestion/languages/typescript/interpret.d.ts +21 -1
- package/dist/core/ingestion/languages/typescript/interpret.js +45 -5
- package/dist/core/ingestion/languages/typescript/module-resolution.d.ts +48 -0
- package/dist/core/ingestion/languages/typescript/module-resolution.js +171 -0
- package/dist/core/ingestion/languages/typescript/query.d.ts +1 -0
- package/dist/core/ingestion/languages/typescript/query.js +303 -14
- package/dist/core/ingestion/languages/typescript/receiver-binding.d.ts +17 -0
- package/dist/core/ingestion/languages/typescript/receiver-binding.js +27 -0
- package/dist/core/ingestion/languages/typescript/scope-resolver.js +27 -40
- package/dist/core/ingestion/languages/typescript/simple-hooks.d.ts +0 -10
- package/dist/core/ingestion/languages/typescript/simple-hooks.js +22 -22
- package/dist/core/ingestion/languages/typescript/tsconfig.d.ts +59 -0
- package/dist/core/ingestion/languages/typescript/tsconfig.js +292 -0
- package/dist/core/ingestion/languages/typescript.js +93 -2
- package/dist/core/ingestion/languages/vue/import-target.d.ts +6 -22
- package/dist/core/ingestion/languages/vue/import-target.js +11 -47
- package/dist/core/ingestion/languages/vue/scope-resolver.js +4 -2
- package/dist/core/ingestion/local-symbol-pruner.js +11 -11
- package/dist/core/ingestion/method-extractors/configs/csharp.js +1 -0
- package/dist/core/ingestion/method-extractors/generic.js +1 -0
- package/dist/core/ingestion/method-types.d.ts +16 -0
- package/dist/core/ingestion/pipeline-phases/di.d.ts +1 -1
- package/dist/core/ingestion/pipeline-phases/di.js +136 -21
- package/dist/core/ingestion/pipeline-phases/index.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/index.js +2 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +15 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +154 -4
- package/dist/core/ingestion/pipeline-phases/parse.js +5 -0
- package/dist/core/ingestion/pipeline-phases/processes.d.ts +3 -2
- package/dist/core/ingestion/pipeline-phases/processes.js +91 -6
- package/dist/core/ingestion/pipeline-phases/routes.js +116 -25
- package/dist/core/ingestion/pipeline-phases/spring-aop.d.ts +34 -0
- package/dist/core/ingestion/pipeline-phases/spring-aop.js +439 -0
- package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.d.ts +34 -0
- package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.js +255 -0
- package/dist/core/ingestion/pipeline-phases/types.d.ts +7 -0
- package/dist/core/ingestion/pipeline.d.ts +26 -2
- package/dist/core/ingestion/pipeline.js +60 -11
- package/dist/core/ingestion/process-processor.d.ts +109 -2
- package/dist/core/ingestion/process-processor.js +464 -52
- package/dist/core/ingestion/resolve-references.d.ts +6 -1
- package/dist/core/ingestion/resolve-references.js +66 -6
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +8 -2
- package/dist/core/ingestion/route-extractors/constant-resolver.js +11 -7
- package/dist/core/ingestion/route-extractors/data-route-table.d.ts +40 -0
- package/dist/core/ingestion/route-extractors/data-route-table.js +876 -0
- package/dist/core/ingestion/route-extractors/dispatch-guard.d.ts +96 -0
- package/dist/core/ingestion/route-extractors/dispatch-guard.js +1184 -0
- package/dist/core/ingestion/route-extractors/java-const-resolver.d.ts +114 -0
- package/dist/core/ingestion/route-extractors/java-const-resolver.js +569 -0
- package/dist/core/ingestion/route-extractors/js-const-resolver.d.ts +192 -0
- package/dist/core/ingestion/route-extractors/js-const-resolver.js +1033 -0
- package/dist/core/ingestion/route-extractors/nest.d.ts +53 -0
- package/dist/core/ingestion/route-extractors/nest.js +523 -0
- package/dist/core/ingestion/route-extractors/spring-shared.d.ts +11 -0
- package/dist/core/ingestion/route-extractors/spring-shared.js +89 -0
- package/dist/core/ingestion/route-extractors/spring.js +214 -29
- package/dist/core/ingestion/scope-extractor-bridge.js +9 -1
- package/dist/core/ingestion/scope-extractor.d.ts +22 -7
- package/dist/core/ingestion/scope-extractor.js +311 -16
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +311 -21
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +16 -5
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +12 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +47 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +20 -15
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +280 -42
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +123 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +171 -17
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +58 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +137 -34
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +30 -1
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +39 -1
- package/dist/core/ingestion/scope-resolution/passes/callable-value-flow.d.ts +18 -1
- package/dist/core/ingestion/scope-resolution/passes/callable-value-flow.js +113 -18
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +107 -5
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +590 -33
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +3 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +101 -41
- package/dist/core/ingestion/scope-resolution/passes/imported-value-refs.d.ts +34 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-value-refs.js +62 -0
- package/dist/core/ingestion/scope-resolution/passes/property-dispatch.d.ts +5 -1
- package/dist/core/ingestion/scope-resolution/passes/property-dispatch.js +8 -1
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +105 -5
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +931 -92
- package/dist/core/ingestion/scope-resolution/passes/return-shape-members.d.ts +58 -0
- package/dist/core/ingestion/scope-resolution/passes/return-shape-members.js +458 -0
- package/dist/core/ingestion/scope-resolution/passes/unique-name-properties.d.ts +180 -0
- package/dist/core/ingestion/scope-resolution/passes/unique-name-properties.js +418 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +29 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +57 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +55 -1
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +241 -19
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +112 -2
- package/dist/core/ingestion/scope-resolution/resolution-outcome.js +38 -1
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +25 -3
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +36 -16
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +210 -9
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +905 -22
- package/dist/core/ingestion/scope-resolution/summary-maps.d.ts +28 -0
- package/dist/core/ingestion/scope-resolution/summary-maps.js +50 -0
- package/dist/core/ingestion/scope-resolution/undecided-satisfaction.d.ts +55 -0
- package/dist/core/ingestion/scope-resolution/undecided-satisfaction.js +37 -0
- package/dist/core/ingestion/scope-resolution/unresolved-receivers.d.ts +109 -0
- package/dist/core/ingestion/scope-resolution/unresolved-receivers.js +204 -0
- package/dist/core/ingestion/scope-resolution/utils/definition-id.d.ts +13 -0
- package/dist/core/ingestion/scope-resolution/utils/definition-id.js +19 -0
- package/dist/core/ingestion/scope-resolution/utils/generic-instantiation.d.ts +171 -0
- package/dist/core/ingestion/scope-resolution/utils/generic-instantiation.js +218 -0
- package/dist/core/ingestion/scope-resolution/workspace-index-types.d.ts +34 -0
- package/dist/core/ingestion/scope-resolution/workspace-index-types.js +15 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +7 -20
- package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -11
- package/dist/core/ingestion/tree-sitter-queries.js +678 -4
- package/dist/core/ingestion/type-env.js +11 -7
- package/dist/core/ingestion/type-extractors/jvm.js +2 -2
- package/dist/core/ingestion/type-extractors/shared.d.ts +17 -0
- package/dist/core/ingestion/type-extractors/shared.js +16 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +12 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +20 -0
- package/dist/core/ingestion/type-extractors/typescript.js +26 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +322 -16
- package/dist/core/ingestion/utils/ast-helpers.js +727 -150
- package/dist/core/ingestion/utils/call-analysis.d.ts +27 -22
- package/dist/core/ingestion/utils/call-analysis.js +202 -7
- package/dist/core/ingestion/utils/callable-flow-captures.d.ts +59 -0
- package/dist/core/ingestion/utils/callable-flow-captures.js +51 -1
- package/dist/core/ingestion/utils/callable-labels.d.ts +25 -0
- package/dist/core/ingestion/utils/callable-labels.js +31 -0
- package/dist/core/ingestion/utils/effective-ram.d.ts +40 -0
- package/dist/core/ingestion/utils/effective-ram.js +58 -0
- package/dist/core/ingestion/utils/line-base.d.ts +14 -0
- package/dist/core/ingestion/utils/line-base.js +14 -0
- package/dist/core/ingestion/utils/method-props.d.ts +27 -4
- package/dist/core/ingestion/utils/method-props.js +29 -4
- package/dist/core/ingestion/utils/receiver-chain-captures.d.ts +31 -0
- package/dist/core/ingestion/utils/receiver-chain-captures.js +57 -0
- package/dist/core/ingestion/utils/receiver-chain-codec.d.ts +89 -0
- package/dist/core/ingestion/utils/receiver-chain-codec.js +182 -0
- package/dist/core/ingestion/utils/scope-tree-walk.d.ts +22 -0
- package/dist/core/ingestion/utils/scope-tree-walk.js +33 -0
- package/dist/core/ingestion/utils/template-arguments.d.ts +96 -0
- package/dist/core/ingestion/utils/template-arguments.js +196 -0
- package/dist/core/ingestion/utils/type-parameters.d.ts +49 -0
- package/dist/core/ingestion/utils/type-parameters.js +206 -0
- package/dist/core/ingestion/workers/callable-id.d.ts +80 -0
- package/dist/core/ingestion/workers/callable-id.js +125 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +14 -1
- package/dist/core/ingestion/workers/parse-worker.js +493 -54
- package/dist/core/ingestion/workers/worker-pool.d.ts +20 -27
- package/dist/core/ingestion/workers/worker-pool.js +91 -3
- package/dist/core/lbug/csv-generator.d.ts +27 -0
- package/dist/core/lbug/csv-generator.js +72 -17
- package/dist/core/lbug/extension-load-error.js +13 -2
- package/dist/core/lbug/extension-loader.d.ts +29 -0
- package/dist/core/lbug/extension-loader.js +31 -6
- package/dist/core/lbug/graph-emit-sink.d.ts +264 -0
- package/dist/core/lbug/graph-emit-sink.js +581 -0
- package/dist/core/lbug/lbug-adapter.d.ts +290 -11
- package/dist/core/lbug/lbug-adapter.js +652 -69
- package/dist/core/lbug/native-check.d.ts +21 -0
- package/dist/core/lbug/native-check.js +248 -32
- package/dist/core/lbug/pdg-emit-sink.d.ts +13 -4
- package/dist/core/lbug/pdg-emit-sink.js +21 -111
- package/dist/core/lbug/pool-adapter.d.ts +9 -0
- package/dist/core/lbug/pool-adapter.js +164 -14
- package/dist/core/lbug/query-batch.d.ts +75 -0
- package/dist/core/lbug/query-batch.js +102 -0
- package/dist/core/lbug/rel-pair-routing.d.ts +160 -2
- package/dist/core/lbug/rel-pair-routing.js +228 -10
- package/dist/core/lbug/schema.d.ts +163 -4
- package/dist/core/lbug/schema.js +404 -159
- package/dist/core/lbug/sync-csv-writer.d.ts +47 -0
- package/dist/core/lbug/sync-csv-writer.js +108 -0
- package/dist/core/run-analyze.d.ts +147 -0
- package/dist/core/run-analyze.js +1215 -179
- package/dist/core/search/bm25-index.d.ts +11 -0
- package/dist/core/search/bm25-index.js +60 -26
- package/dist/core/search/fts-indexes.d.ts +150 -9
- package/dist/core/search/fts-indexes.js +241 -23
- package/dist/core/wiki/graph-queries.js +145 -112
- package/dist/core/wiki/llm-client.d.ts +29 -3
- package/dist/core/wiki/llm-client.js +47 -12
- package/dist/core/wiki/prompts.d.ts +19 -0
- package/dist/core/wiki/prompts.js +20 -1
- package/dist/lib/utils.d.ts +113 -0
- package/dist/lib/utils.js +185 -0
- package/dist/mcp/local/aop-metadata.d.ts +77 -0
- package/dist/mcp/local/aop-metadata.js +227 -0
- package/dist/mcp/local/bean-metadata.d.ts +2 -1
- package/dist/mcp/local/bean-metadata.js +22 -1
- package/dist/mcp/local/convex-metadata.d.ts +8 -0
- package/dist/mcp/local/convex-metadata.js +45 -0
- package/dist/mcp/local/local-backend.d.ts +174 -11
- package/dist/mcp/local/local-backend.js +1678 -331
- package/dist/mcp/local/path-predicate.d.ts +19 -0
- package/dist/mcp/local/path-predicate.js +21 -0
- package/dist/mcp/local/pdg-impact.d.ts +132 -10
- package/dist/mcp/local/pdg-impact.js +391 -98
- package/dist/mcp/repository-policy.d.ts +1 -0
- package/dist/mcp/repository-policy.js +5 -0
- package/dist/mcp/resources.js +26 -4
- package/dist/mcp/server.js +23 -13
- package/dist/mcp/tools.d.ts +8 -0
- package/dist/mcp/tools.js +76 -23
- package/dist/server/analyze-job.d.ts +37 -2
- package/dist/server/analyze-job.js +13 -2
- package/dist/server/analyze-launch.d.ts +1 -1
- package/dist/server/analyze-launch.js +79 -10
- package/dist/server/analyze-worker-core.d.ts +1 -1
- package/dist/server/analyze-worker-core.js +10 -1
- package/dist/server/analyze-worker-ipc.d.ts +1 -1
- package/dist/server/analyze-worker-ipc.js +4 -0
- package/dist/server/analyze-worker-protocol.d.ts +59 -0
- package/dist/server/analyze-worker-protocol.js +1 -0
- package/dist/server/analyze-worker.d.ts +1 -17
- package/dist/server/api.d.ts +2 -11
- package/dist/server/api.js +164 -163
- package/dist/server/embed-run-outcome.d.ts +89 -0
- package/dist/server/embed-run-outcome.js +88 -0
- package/dist/server/git-clone.js +6 -2
- package/dist/server/middleware.d.ts +116 -9
- package/dist/server/middleware.js +291 -22
- package/dist/server/sse-progress.d.ts +41 -0
- package/dist/server/sse-progress.js +125 -0
- package/dist/storage/branch-index.d.ts +11 -6
- package/dist/storage/branch-index.js +15 -7
- package/dist/storage/file-hash.js +2 -2
- package/dist/storage/fs-atomic.d.ts +25 -0
- package/dist/storage/fs-atomic.js +42 -0
- package/dist/storage/git.d.ts +131 -0
- package/dist/storage/git.js +236 -2
- package/dist/storage/index-lock.d.ts +102 -0
- package/dist/storage/index-lock.js +660 -0
- package/dist/storage/parse-cache.js +588 -3
- package/dist/storage/parsedfile-store.js +76 -6
- package/dist/storage/repo-manager.d.ts +54 -435
- package/dist/storage/repo-manager.js +312 -215
- package/dist/storage/repo-meta.d.ts +530 -0
- package/dist/storage/repo-meta.js +100 -0
- package/dist/types/pipeline.d.ts +37 -0
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +1 -1
- package/hooks/claude/gitnexus-hook.cjs +1 -1
- package/hooks/claude/resolve-analyze-cmd.cjs +80 -13
- package/package.json +11 -2
- package/scripts/cross-platform-shard.ts +153 -0
- package/scripts/cross-platform-tests.ts +122 -0
- package/scripts/run-cross-platform.ts +16 -5
- package/skills/gitnexus-cli.md +3 -3
- package/skills/gitnexus-debugging.md +37 -7
- package/skills/gitnexus-exploring.md +26 -4
- package/skills/gitnexus-impact-analysis.md +71 -10
- package/skills/gitnexus-plan/README.md +11 -6
- package/skills/gitnexus-plan/references/evidence-provenance.md +49 -12
- package/skills/gitnexus-plan/scripts/evidence-provenance.mjs +654 -372
- package/skills/gitnexus-refactoring.md +48 -5
- package/skills/gitnexus-review/SKILL.md +12 -6
- package/skills/gitnexus-review/ci-personas/ci-adversarial-lens.md +1 -1
- package/skills/gitnexus-review/ci-personas/ci-blast-radius-lens.md +1 -1
- package/skills/gitnexus-review/ci-personas/ci-correctness-lens.md +1 -1
- package/skills/gitnexus-review/ci-personas/ci-coverage-lens.md +1 -1
- package/skills/gitnexus-review/ci-personas/ci-critic-lens.md +1 -1
- package/skills/gitnexus-review/ci-personas/ci-security-lens.md +1 -1
- package/skills/gitnexus-taint-analysis.md +13 -7
- package/skills/gitnexus-work/SKILL.md +4 -1
- package/skills/gitnexus-work/references/evidence-provenance.md +49 -12
- package/skills/gitnexus-work/scripts/evidence-provenance.mjs +654 -372
- package/vendor/leiden/utils.cjs +28 -1
- package/web/assets/ProcessFlowModal-Badh2Pbg.js +24 -0
- package/web/assets/{__vite-browser-external-bMKyYQkO.js → __vite-browser-external-COxE8F0G.js} +1 -1
- package/web/assets/abnfDiagram-QNN2D4NM-Cq2djWWe.js +1 -0
- package/web/assets/agent-Dr4l5EOp.js +662 -0
- package/web/assets/architecture-TIHT7OUA-V6N4J3SY-fTePvLoe.js +1 -0
- package/web/assets/architectureDiagram-OT7VKSJ7-mKEXxkeF.js +36 -0
- package/web/assets/{blockDiagram-7IZFK4PR-DBefcjof.js → blockDiagram-54WLDVWM-Dg_hrRWE.js} +4 -4
- package/web/assets/{c4Diagram-Y2BXMSZH-EjWnjEJl.js → c4Diagram-X2LASM7V-CnhoT4aV.js} +2 -2
- package/web/assets/chunk-224SPVON-B8gxtXpy.js +1 -0
- package/web/assets/chunk-2AEHWXPW-CV4r6-1I.js +127 -0
- package/web/assets/chunk-2SREHG4O-Dip42xOq.js +1 -0
- package/web/assets/chunk-4RFN2BYJ-DHHWuC-n.js +1 -0
- package/web/assets/chunk-66DQ2XMT-CM0xjFAN.js +1 -0
- package/web/assets/chunk-6BELYETK-ChsYtfHW.js +1 -0
- package/web/assets/{chunk-LII3EMHJ-BL6lKJ_P.js → chunk-6L755F7B-CMhLBxi2.js} +1 -1
- package/web/assets/{chunk-QA3QBVWF-DVXPOIxe.js → chunk-7CWYLC5S-DRgGkkpF.js} +1 -1
- package/web/assets/{chunk-ZXARS5L4-C3v-4Fsw.js → chunk-A7VWPJGB-B4O3oQdQ.js} +1 -1
- package/web/assets/chunk-BI6VK774-D0oaWk8V.js +1 -0
- package/web/assets/chunk-D6VWDJW2-BxcBdZ9c.js +2 -0
- package/web/assets/chunk-DMV4VAQV-Bn6WXDSX.js +4 -0
- package/web/assets/chunk-E5QJAATJ-dh-5CtIV.js +2 -0
- package/web/assets/chunk-EQFTRU2I-1ix7TzW9.js +2 -0
- package/web/assets/chunk-FWYVLQTC-BgDItRzJ.js +1 -0
- package/web/assets/chunk-HNC4WDU7-DoU3KnA7.js +1 -0
- package/web/assets/chunk-L5GCOVLC-CE2w8Nm_.js +161 -0
- package/web/assets/chunk-LAHSG3N2-Bz4hcn0N.js +321 -0
- package/web/assets/chunk-LORZLZWG-B57uGw2N.js +88 -0
- package/web/assets/chunk-NLANEA3F-XpH_NPA5.js +62 -0
- package/web/assets/chunk-NV3KIAZN-5yvcpMiu.js +1 -0
- package/web/assets/chunk-OIYT25JQ-CQ6KOZGX.js +1 -0
- package/web/assets/chunk-PE7DX7ZZ-CCvRVj43.js +10 -0
- package/web/assets/chunk-Q67WD55A-BWCH2SVz.js +156 -0
- package/web/assets/chunk-RRFMTAIC-CMN0wcYA.js +1 -0
- package/web/assets/{chunk-RLI5ZMPA-CkXZ0y0U.js → chunk-SATU7PGQ-B1zhgOvc.js} +1 -1
- package/web/assets/chunk-STOV2HOB-GnExfzIX.js +1 -0
- package/web/assets/{chunk-INKRHTLW-Ckl3_KkO.js → chunk-SZD42YQK-ijyKKVr1.js} +21 -21
- package/web/assets/{chunk-LCXTWHL2-C162wE5z.js → chunk-TFPGFQPA-Dj6a-0pC.js} +3 -3
- package/web/assets/chunk-TGVD4F4B-BMc31Gj2.js +2 -0
- package/web/assets/chunk-TZUXEDM2-B7p9AcTw.js +1 -0
- package/web/assets/chunk-UAT7B5JY-DW-A_fpI.js +1 -0
- package/web/assets/{chunk-6764PJDD-C7xxk063.js → chunk-VE5CLXGZ-C1vB7wpY.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-ZNgyLFq6.js → chunk-VJFVEZTB-CX66F2ep.js} +3 -3
- package/web/assets/chunk-VY5UBI4V-yxPQAWxw.js +1 -0
- package/web/assets/chunk-WJBAP47W-BckNkuEN.js +1 -0
- package/web/assets/chunk-XNMVGMAZ-Cg_LnPtj.js +1 -0
- package/web/assets/classDiagram-VPOSVPXH-BjOiXA_C.js +1 -0
- package/web/assets/classDiagram-v2-5VFNUXQA-BjOiXA_C.js +1 -0
- package/web/assets/{cose-bilkent-UX7MHV2Q-BmnZ7Mvj.js → cose-bilkent-RZA4XIPR-BHpK4Crp.js} +1 -1
- package/web/assets/cynefin-VYW2F7L2-2VAHYKR6-DAsF6GZv.js +1 -0
- package/web/assets/cynefinDiagram-TR7LZUCG-C3Iar9lZ.js +62 -0
- package/web/assets/dagre-K64A6Z3X-CUwi79mA.js +4 -0
- package/web/assets/diagram-2TLGJV2N-C2F5Nfym.js +30 -0
- package/web/assets/diagram-JCD7PQ7C-DC5-9FRr.js +24 -0
- package/web/assets/{diagram-HNR7UZ2L-DVRltWbL.js → diagram-KFBZAVDW-Dfmf_4ti.js} +3 -3
- package/web/assets/diagram-L7DVE3PN-GIRyVUtt.js +41 -0
- package/web/assets/diagram-YP3FGXHI-CyNFYtkd.js +24 -0
- package/web/assets/ebnfDiagram-DRHRQJWO-D8_CI4ZF.js +1 -0
- package/web/assets/erDiagram-SZLJLQMB-CohQjznC.js +85 -0
- package/web/assets/error-D9MtuG9s.js +1 -0
- package/web/assets/eventmodeling-45OFAUF4-AOUJWSLX-BvTc6iQi.js +1 -0
- package/web/assets/flowDiagram-BWE6NHOH-B6RnEvD9.js +1 -0
- package/web/assets/ganttDiagram-2QTCJSNI-Cks_mEcF.js +292 -0
- package/web/assets/gitGraph-TEB2WS4Q-U5VWYIBC-Dy7CCSZ5.js +1 -0
- package/web/assets/gitGraphDiagram-VVZAVV5G-CG41cIVc.js +106 -0
- package/web/assets/{index-D7mTFcvQ.js → index-2zdvEdzg.js} +51 -51
- package/web/assets/index-CJDpyH-n.css +2 -0
- package/web/assets/info-DKCQHKI2-DX6KST54-BLJg1ikX.js +1 -0
- package/web/assets/infoDiagram-FY4PZKRE-BJ3vPVwW.js +2 -0
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-XpjJRU9h.js → ishikawaDiagram-4JDSWWDM-DmqWtjkl.js} +2 -2
- package/web/assets/{journeyDiagram-M6C3CM3L-C6wjooas.js → journeyDiagram-QUS3FAKT-XAzVzrQW.js} +3 -3
- package/web/assets/{kanban-definition-75IXJCU3-5_UVY79g.js → kanban-definition-D4L7SQKK-YurRnmYn.js} +6 -6
- package/web/assets/katex-C5OPUE3Q-Bnbavtmu.js +257 -0
- package/web/assets/{mindmap-definition-2TDM6QVE-8xDLzk2e.js → mindmap-definition-66QP5TD6-ChrWWnil.js} +5 -5
- package/web/assets/node.browser-U-KyT5AW.js +1 -0
- package/web/assets/packet-7NZHBO7P-5WG45QPS-KUxcuvaA.js +1 -0
- package/web/assets/pegDiagram-YKIO5C6U-QGeQLEq2.js +1 -0
- package/web/assets/pie-RZYD4A2V-2OCZ2VTD-DPs08kJY.js +1 -0
- package/web/assets/pieDiagram-YPY3TKG5-BuT3MYzm.js +39 -0
- package/web/assets/{quadrantDiagram-VICAPDV7-DR54jOeR.js → quadrantDiagram-VO5QLCXA-B7098wSc.js} +3 -3
- package/web/assets/radar-I7S5WNFK-EPLSA35B-w5F0K3a7.js +1 -0
- package/web/assets/railroad-3IZDKUUU-PWFQV6Y6-Bu668yqY.js +1 -0
- package/web/assets/railroad-abnf-AHOZXSZD-JYYJX3BS-B6ghpETS.js +1 -0
- package/web/assets/railroad-ebnf-EBAXGLYW-RBZ4ONAP-D-XmHh3M.js +1 -0
- package/web/assets/railroad-peg-LSFZ7HO6-KHNPXGPG-mhkrhzEn.js +1 -0
- package/web/assets/railroadDiagram-M77LTMSB-AXu4vxk_.js +1 -0
- package/web/assets/{requirementDiagram-JXO7QTGE-PEkg7sKC.js → requirementDiagram-RASO5HTO-B-l8InpK.js} +4 -4
- package/web/assets/{sankeyDiagram-URQDO5SZ-DDWEtjcs.js → sankeyDiagram-F32LOJ6L-Kd7zjJgX.js} +3 -3
- package/web/assets/{sequenceDiagram-VS2MUI6T-Bv-uwANx.js → sequenceDiagram-URATNSBD-BlcdY9DS.js} +7 -7
- package/web/assets/sizeCapture-5GORAGP4-BEqQzsxs.js +1 -0
- package/web/assets/src-C5sx_gMp.js +1 -0
- package/web/assets/stateDiagram-DODFXNG5-DTdHc-fF.js +1 -0
- package/web/assets/stateDiagram-v2-JNMM5JRA-DioNLDDo.js +1 -0
- package/web/assets/swimlanes-OSWOYLSK-CA0ofdwY.js +2 -0
- package/web/assets/swimlanesDiagram-RHGX2RDE-PkwUdckC.js +8 -0
- package/web/assets/{timeline-definition-O6YCAMPW-Qmsuo7qS.js → timeline-definition-WJXWRH4I-DUfp1R3E.js} +3 -3
- package/web/assets/treeView-QDETBFTQ-EHYTSQNO-C5lpBgPm.js +1 -0
- package/web/assets/treemap-6X3UGDF4-CCWCF7EK-BB0ZyO-B.js +1 -0
- package/web/assets/vennDiagram-K3VCMQLI-Bj-FN-XS.js +34 -0
- package/web/assets/wardley-OPB4EBWU-6OWERDVT-CEA3Nq0j.js +1 -0
- package/web/assets/wardleyDiagram-XK6FUAWV-CVV0dHFr.js +78 -0
- package/web/assets/xychartDiagram-5VRUVLCW-CK_rbSWE.js +7 -0
- package/web/index.html +16 -15
- package/dist/core/ingestion/languages/python/index-stats.d.ts +0 -18
- package/dist/core/ingestion/languages/python/index-stats.js +0 -25
- package/web/assets/ProcessFlowModal-CwkpVkv-.js +0 -22
- package/web/assets/agent-Ds-lkZjX.js +0 -626
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-BVWFvYz2.js +0 -1
- package/web/assets/architectureDiagram-UL44E2DR-CmG3uPWf.js +0 -36
- package/web/assets/chunk-2T2R6R2M-Bb6Sy3_j.js +0 -4
- package/web/assets/chunk-2UTLFMKG-CXlhF3Pt.js +0 -1
- package/web/assets/chunk-3SSMPTDK-rPsRM1x0.js +0 -321
- package/web/assets/chunk-4R4BOZG6-Be_F0JoX.js +0 -159
- package/web/assets/chunk-67TQ5CYL-CA7OhIEf.js +0 -124
- package/web/assets/chunk-7J6CGLKN-BlKwgXKs.js +0 -10
- package/web/assets/chunk-C62D2QBJ-3gdL8EKc.js +0 -1
- package/web/assets/chunk-CEXFNPSA-CjwtMDkc.js +0 -1
- package/web/assets/chunk-J5EP6P6S-L19oz55R.js +0 -1
- package/web/assets/chunk-KGFNY3KK-BZeP-55u.js +0 -62
- package/web/assets/chunk-KGYTTC2M-zm6n242I.js +0 -1
- package/web/assets/chunk-KNLZD3CH-BGlP6X9R.js +0 -1
- package/web/assets/chunk-LRIF4GLE-n59lhprL.js +0 -1
- package/web/assets/chunk-RERM46MO-B5dAdWpP.js +0 -1
- package/web/assets/chunk-RKZBBQEN-MyIQbGgD.js +0 -1
- package/web/assets/chunk-T2UQINTJ-BEyXhZWe.js +0 -1
- package/web/assets/chunk-UP6H54XL-BG-n7qDp.js +0 -1
- package/web/assets/chunk-UXSXWOXI-CW1_r5cS.js +0 -1
- package/web/assets/chunk-UY5QBCOK-ByqTVLvh.js +0 -1
- package/web/assets/chunk-VU6ZFW4Y-BLeasdkU.js +0 -2
- package/web/assets/classDiagram-KGZ6W3CR-olpO3fMe.js +0 -1
- package/web/assets/classDiagram-v2-72OJOZXJ-olpO3fMe.js +0 -1
- package/web/assets/dagre-ND4H6XIP-CY4iIF7v.js +0 -4
- package/web/assets/diagram-3NCE3AQN-CO6QpzEE.js +0 -43
- package/web/assets/diagram-GF46GFSD-B2atbzmU.js +0 -24
- package/web/assets/diagram-QXG6HAR7-DR6AHD2h.js +0 -24
- package/web/assets/diagram-WEQXMOUZ-DkpoMwTL.js +0 -10
- package/web/assets/erDiagram-L5TCEMPS-n8g_90bc.js +0 -85
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-CgFBM7e-.js +0 -1
- package/web/assets/flowDiagram-H6V6AXG4-SGMDiCkB.js +0 -162
- package/web/assets/ganttDiagram-JCBTUEKG-Cb3UBqRS.js +0 -292
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DCJv7SII.js +0 -1
- package/web/assets/gitGraphDiagram-S2ZK5IYY-Bs9CR7tq.js +0 -106
- package/web/assets/index-CX_fADmQ.css +0 -2
- package/web/assets/info-J43DQDTF-KCYPFFUO-BfNy-9Nd.js +0 -1
- package/web/assets/infoDiagram-3YFTVSEB-T368on4D.js +0 -2
- package/web/assets/katex-K3KEBU37-qRQBy-EZ.js +0 -261
- package/web/assets/node-Ct_zna8U.js +0 -8
- package/web/assets/packet-YPE3B663-LP52Z2RK-D4qkeG9k.js +0 -1
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-B_yMKyQa.js +0 -1
- package/web/assets/pieDiagram-CU6KROY3-Bv4LpAbr.js +0 -30
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DeD6yN_0.js +0 -1
- package/web/assets/sdk-CzHHH8tD.js +0 -37
- package/web/assets/src-Cv1eWg0o.js +0 -1
- package/web/assets/stateDiagram-7D4R322I-DVNjmkEq.js +0 -1
- package/web/assets/stateDiagram-v2-36443NZ5-CyN4Fh7_.js +0 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-CDuSYnd4.js +0 -1
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-imazucyJ.js +0 -1
- package/web/assets/vennDiagram-MWXL3ELB-BD1iK1SX.js +0 -34
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-DqrT9tB8.js +0 -1
- package/web/assets/wardleyDiagram-CUQ6CDDI-D6TzLVob.js +0 -78
- package/web/assets/xychartDiagram-N2JHSOCM-CoZ_Tits.js +0 -7
- /package/web/assets/{chunk-7W6UQGC5-pyKGssV3.js → chunk-LIEV3EAG-pyKGssV3.js} +0 -0
|
@@ -479,11 +479,11 @@ function resolveOwnGitTopLevel(absolute) {
|
|
|
479
479
|
if (result.status !== 0) return null;
|
|
480
480
|
let topLevel;
|
|
481
481
|
try {
|
|
482
|
-
topLevel = fs.realpathSync(decodeUtf8(result.stdout, 'nested repository root').trim());
|
|
482
|
+
topLevel = fs.realpathSync.native(decodeUtf8(result.stdout, 'nested repository root').trim());
|
|
483
483
|
} catch {
|
|
484
484
|
return null;
|
|
485
485
|
}
|
|
486
|
-
return topLevel === fs.realpathSync(absolute) ? topLevel : null;
|
|
486
|
+
return topLevel === fs.realpathSync.native(absolute) ? topLevel : null;
|
|
487
487
|
}
|
|
488
488
|
|
|
489
489
|
function readOwnGitlinkHead(absolute) {
|
|
@@ -616,17 +616,30 @@ function filesystemObject(absolute, expectedKind, mutationGuards, testHooks) {
|
|
|
616
616
|
throw new Error(`Unsupported filesystem object at ${absolute}`);
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
-
|
|
619
|
+
// Every dirty path re-walks its own parents, and dirty paths overwhelmingly
|
|
620
|
+
// share them — the repository root is re-stat'ed once per path. `guarded` is
|
|
621
|
+
// per-snapshot and remembers which absolute directories already carry a guard,
|
|
622
|
+
// so each distinct directory is stat'ed and guarded exactly once.
|
|
623
|
+
//
|
|
624
|
+
// Keeping the first-seen identity is the conservative choice: verifyGuards
|
|
625
|
+
// re-checks every guard against the filesystem at the end, so a directory that
|
|
626
|
+
// changes after it was guarded still fails there. Skipping a re-stat cannot hide
|
|
627
|
+
// a change; it only avoids recording the same directory twice.
|
|
628
|
+
function guardPathParents(repo, repoPath, mutationGuards, guarded) {
|
|
620
629
|
const components = repoPath.split('/');
|
|
621
630
|
let current = repo;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
631
|
+
if (!guarded.has(repo)) {
|
|
632
|
+
guarded.add(repo);
|
|
633
|
+
mutationGuards.push({
|
|
634
|
+
type: 'directory',
|
|
635
|
+
absolute: repo,
|
|
636
|
+
identity: stableDirectoryIdentity(fs.lstatSync(repo, { bigint: true })),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
628
639
|
for (const component of components.slice(0, -1)) {
|
|
629
640
|
current = path.join(current, component);
|
|
641
|
+
// Already proved a real directory and already guarded on an earlier path.
|
|
642
|
+
if (guarded.has(current)) continue;
|
|
630
643
|
let stat;
|
|
631
644
|
try {
|
|
632
645
|
stat = fs.lstatSync(current, { bigint: true });
|
|
@@ -638,6 +651,7 @@ function guardPathParents(repo, repoPath, mutationGuards) {
|
|
|
638
651
|
throw new Error(`Refusing to traverse symlink parent for ${repoPath}`);
|
|
639
652
|
}
|
|
640
653
|
if (!stat.isDirectory()) return;
|
|
654
|
+
guarded.add(current);
|
|
641
655
|
mutationGuards.push({
|
|
642
656
|
type: 'directory',
|
|
643
657
|
absolute: current,
|
|
@@ -646,81 +660,153 @@ function guardPathParents(repo, repoPath, mutationGuards) {
|
|
|
646
660
|
}
|
|
647
661
|
}
|
|
648
662
|
|
|
649
|
-
|
|
663
|
+
// A bound, not a bug: the absence cache deduplicates correctly and leaks nothing,
|
|
664
|
+
// but citedPaths is caller-supplied and unbounded, so a pathological snapshot
|
|
665
|
+
// could hold more descriptors than the process is allowed (macOS
|
|
666
|
+
// kern.maxfilesperproc is 24576). The peak precedes a `git` spawn, so exhaustion
|
|
667
|
+
// would surface as a git failure misreported as evidence instability.
|
|
668
|
+
//
|
|
669
|
+
// Refuse rather than evict: closing a cached descriptor would silently break the
|
|
670
|
+
// pinned chain of an absence guard that was already recorded against it, which is
|
|
671
|
+
// exactly the inode-recycling hole the pins exist to close.
|
|
672
|
+
const ABSENCE_ANCHOR_LIMITS = Object.freeze({ maxPinnedDirectories: 4096 });
|
|
673
|
+
|
|
674
|
+
// Every no-follow read and every exclusive create in this file uses one of these
|
|
675
|
+
// two, so a change lands in one place rather than in seven.
|
|
676
|
+
const VERIFIED_READ_FLAGS =
|
|
677
|
+
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0);
|
|
678
|
+
const VERIFIED_CREATE_FLAGS =
|
|
679
|
+
fs.constants.O_RDWR |
|
|
680
|
+
fs.constants.O_CREAT |
|
|
681
|
+
fs.constants.O_EXCL |
|
|
682
|
+
fs.constants.O_NOFOLLOW |
|
|
683
|
+
(fs.constants.O_CLOEXEC ?? 0);
|
|
684
|
+
|
|
685
|
+
function requireAbsenceAnchorCapacity(cache) {
|
|
686
|
+
if (cache.size >= ABSENCE_ANCHOR_LIMITS.maxPinnedDirectories) {
|
|
687
|
+
throw new Error(
|
|
688
|
+
`Absence anchoring exceeds ${ABSENCE_ANCHOR_LIMITS.maxPinnedDirectories} pinned directories`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const ANCHORED_DIRECTORY_FLAGS =
|
|
694
|
+
fs.constants.O_RDONLY |
|
|
695
|
+
fs.constants.O_DIRECTORY |
|
|
696
|
+
fs.constants.O_NOFOLLOW |
|
|
697
|
+
(fs.constants.O_CLOEXEC ?? 0);
|
|
698
|
+
|
|
699
|
+
// Every absence receipt is verified long after its walk returns, so the chain
|
|
700
|
+
// that produced it has to stay pinned until the snapshot ends — an unpinned inode
|
|
701
|
+
// number can be recycled by a replacement directory that then reproduces the
|
|
702
|
+
// recorded identity exactly. Absent cited paths overwhelmingly share prefixes, so
|
|
703
|
+
// the walked directories are cached per snapshot and keyed by repo-relative
|
|
704
|
+
// prefix: one open descriptor and one anchored walk per distinct directory rather
|
|
705
|
+
// than per path. snapshotEvidence owns every descriptor in this cache and closes
|
|
706
|
+
// each exactly once; guards only borrow them for verification.
|
|
707
|
+
function anchoredAbsenceRoot(repo, cache) {
|
|
708
|
+
const cached = cache.get('');
|
|
709
|
+
if (cached) return cached;
|
|
710
|
+
requireAbsenceAnchorCapacity(cache);
|
|
711
|
+
const fd = openVerifiedDirectory(repo, ANCHORED_DIRECTORY_FLAGS);
|
|
712
|
+
const handle = {
|
|
713
|
+
fd,
|
|
714
|
+
expectedPath: repo,
|
|
715
|
+
chain: [
|
|
716
|
+
{ expectedPath: repo, identity: stableDirectoryIdentity(fs.fstatSync(fd, { bigint: true })) },
|
|
717
|
+
],
|
|
718
|
+
descriptors: [fd],
|
|
719
|
+
};
|
|
720
|
+
cache.set('', handle);
|
|
721
|
+
return handle;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function recordAnchoredAbsence(repo, repoPath, mutationGuards, cache) {
|
|
650
725
|
requireDescriptorAnchoring();
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
mutationGuards.push({
|
|
676
|
-
type: 'absence',
|
|
677
|
-
fd: retainedFd,
|
|
678
|
-
childName: component,
|
|
679
|
-
repoPath,
|
|
680
|
-
parentIdentity: stableDirectoryIdentity(parentStat),
|
|
681
|
-
parentMutationIdentity: statIdentity(parentStat),
|
|
682
|
-
});
|
|
683
|
-
for (const fd of descriptors) {
|
|
684
|
-
if (fd !== retainedFd) fs.closeSync(fd);
|
|
685
|
-
}
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
if (index === components.length - 1) {
|
|
689
|
-
throw new Error(`${repoPath} appeared while its absence was being anchored`);
|
|
690
|
-
}
|
|
691
|
-
if (childStat.isSymbolicLink() || !childStat.isDirectory()) {
|
|
692
|
-
throw new Error(`Refusing a non-directory parent while anchoring absence for ${repoPath}`);
|
|
726
|
+
const components = repoPath.split('/');
|
|
727
|
+
let handle = anchoredAbsenceRoot(repo, cache);
|
|
728
|
+
let prefix = '';
|
|
729
|
+
for (let index = 0; index < components.length; index += 1) {
|
|
730
|
+
const component = components[index];
|
|
731
|
+
const isFinal = index === components.length - 1;
|
|
732
|
+
prefix = prefix === '' ? component : `${prefix}/${component}`;
|
|
733
|
+
// The final component is always re-checked against the filesystem: it is the
|
|
734
|
+
// one whose absence is being recorded, and a cached answer would be a stale
|
|
735
|
+
// one. Only the prefix directories are reused.
|
|
736
|
+
const cached = isFinal ? undefined : cache.get(prefix);
|
|
737
|
+
if (cached) {
|
|
738
|
+
handle = cached;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const child = anchoredChild(handle, component);
|
|
742
|
+
let childStat;
|
|
743
|
+
try {
|
|
744
|
+
childStat = lstatChild(child);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error;
|
|
747
|
+
const parentStat = fs.fstatSync(handle.fd, { bigint: true });
|
|
748
|
+
if (!parentStat.isDirectory()) {
|
|
749
|
+
throw new Error(`Absence parent is no longer a directory for ${repoPath}`);
|
|
693
750
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
751
|
+
mutationGuards.push({
|
|
752
|
+
type: 'absence',
|
|
753
|
+
// The handle is the holder the guard verifies against, and `ref` is the
|
|
754
|
+
// child path already built through the anchoredChild chokepoint — the
|
|
755
|
+
// guard must never re-derive that name itself.
|
|
756
|
+
handle,
|
|
757
|
+
ref: child,
|
|
758
|
+
fd: handle.fd,
|
|
759
|
+
repoPath,
|
|
760
|
+
parentMutationIdentity: statIdentity(parentStat),
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
697
763
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
764
|
+
if (isFinal) {
|
|
765
|
+
throw new Error(`${repoPath} appeared while its absence was being anchored`);
|
|
766
|
+
}
|
|
767
|
+
if (childStat.isSymbolicLink() || !childStat.isDirectory()) {
|
|
768
|
+
throw new Error(`Refusing a non-directory parent while anchoring absence for ${repoPath}`);
|
|
769
|
+
}
|
|
770
|
+
requireAbsenceAnchorCapacity(cache);
|
|
771
|
+
const childFd = openVerifiedDirectory(child.path, ANCHORED_DIRECTORY_FLAGS);
|
|
772
|
+
const expectedPath = path.join(handle.expectedPath, component);
|
|
773
|
+
let next;
|
|
774
|
+
try {
|
|
775
|
+
if (!anchoringBackend().descriptorMatchesChild(childFd, expectedPath, childStat)) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
`Absence parent descriptor does not match its verified inode for ${repoPath}`,
|
|
778
|
+
);
|
|
706
779
|
}
|
|
780
|
+
next = {
|
|
781
|
+
fd: childFd,
|
|
782
|
+
expectedPath,
|
|
783
|
+
chain: [...handle.chain, { expectedPath, identity: stableDirectoryIdentity(childStat) }],
|
|
784
|
+
descriptors: [...handle.descriptors, childFd],
|
|
785
|
+
};
|
|
786
|
+
} catch (error) {
|
|
787
|
+
fs.closeSync(childFd);
|
|
788
|
+
throw error;
|
|
707
789
|
}
|
|
708
|
-
|
|
790
|
+
cache.set(prefix, next);
|
|
791
|
+
handle = next;
|
|
709
792
|
}
|
|
793
|
+
throw new Error(`Could not anchor absence for ${repoPath}`);
|
|
710
794
|
}
|
|
711
795
|
|
|
712
|
-
function materializeRecord(repo, statusRecord, layers, mutationGuards, testHooks) {
|
|
796
|
+
function materializeRecord(repo, statusRecord, layers, mutationGuards, testHooks, walkState) {
|
|
713
797
|
const head = layers.head(statusRecord.path);
|
|
714
798
|
const index = layers.index(statusRecord.path);
|
|
715
799
|
const expectedKind = index.kind === 'gitlink' || head.kind === 'gitlink' ? 'gitlink' : null;
|
|
716
|
-
guardPathParents(repo, statusRecord.path, mutationGuards);
|
|
800
|
+
guardPathParents(repo, statusRecord.path, mutationGuards, walkState.guardedDirectories);
|
|
717
801
|
const filesystem = filesystemObject(
|
|
718
802
|
path.join(repo, ...statusRecord.path.split('/')),
|
|
719
803
|
expectedKind,
|
|
720
804
|
mutationGuards,
|
|
721
805
|
testHooks,
|
|
722
806
|
);
|
|
723
|
-
if (filesystem.kind === ABSENT)
|
|
807
|
+
if (filesystem.kind === ABSENT) {
|
|
808
|
+
recordAnchoredAbsence(repo, statusRecord.path, mutationGuards, walkState.absenceCache);
|
|
809
|
+
}
|
|
724
810
|
if (statusRecord.directory_hint && filesystem.kind !== 'directory') {
|
|
725
811
|
throw new Error(
|
|
726
812
|
`Git reported an embedded directory but found ${filesystem.kind}: ${statusRecord.path}`,
|
|
@@ -789,9 +875,15 @@ export function serializeDirtyRecords(entries) {
|
|
|
789
875
|
}
|
|
790
876
|
|
|
791
877
|
function assertRepository(repoInput) {
|
|
792
|
-
|
|
878
|
+
// realpathSync.native, not realpathSync: the JS resolver preserves a Windows
|
|
879
|
+
// 8.3 short component (C:\Users\RUNNER~1\...) while git always reports the long
|
|
880
|
+
// form, so the two would never compare equal and every caller would be told the
|
|
881
|
+
// worktree root is not the worktree root it just named.
|
|
882
|
+
const repo = fs.realpathSync.native(requireString(repoInput, 'repo'));
|
|
793
883
|
const topLevelResult = git(repo, ['rev-parse', '--show-toplevel']);
|
|
794
|
-
const topLevel = fs.realpathSync
|
|
884
|
+
const topLevel = fs.realpathSync.native(
|
|
885
|
+
decodeUtf8(topLevelResult.stdout, 'repository root').trim(),
|
|
886
|
+
);
|
|
795
887
|
if (topLevel !== repo) throw new Error(`--repo must be the Git worktree root (${topLevel})`);
|
|
796
888
|
return repo;
|
|
797
889
|
}
|
|
@@ -882,17 +974,48 @@ function stableFileIdentity(stat) {
|
|
|
882
974
|
return [stat.dev, stat.ino, stat.mode, stat.size].map(String).join(':');
|
|
883
975
|
}
|
|
884
976
|
|
|
977
|
+
// The two backends below differ in one decisive way, and it is worth stating
|
|
978
|
+
// plainly because the security properties are not the same.
|
|
979
|
+
//
|
|
980
|
+
// Linux ANCHORS. A name is resolved through /proc/self/fd/<fd>/<child>, which
|
|
981
|
+
// starts the walk at the inode the descriptor holds, so a parent that is renamed
|
|
982
|
+
// away cannot be traversed at all: the descriptor keeps pointing at the original
|
|
983
|
+
// directory and the impostor planted at the same name is simply never reached.
|
|
984
|
+
//
|
|
985
|
+
// macOS VERIFIES. Node cannot resolve a name relative to a descriptor there —
|
|
986
|
+
// /dev/fd/<fd> is not a magic link (it stats as the directory but every attempt
|
|
987
|
+
// to traverse a child through it returns ENOENT), and fcntl F_GETPATH is a
|
|
988
|
+
// name-cache snapshot rather than a live anchor. So the Darwin backend resolves
|
|
989
|
+
// lexically, holds an open descriptor on every element of the chain, and proves
|
|
990
|
+
// before and after each operation that the path chain still names exactly the
|
|
991
|
+
// inodes it is holding. That DETECTS a swapped parent and aborts the write; it
|
|
992
|
+
// does not make the swap impossible the way the Linux path does. A swap landing
|
|
993
|
+
// inside the window between a check and the call it guards is caught by the
|
|
994
|
+
// following check, after the fact, rather than being unreachable.
|
|
995
|
+
//
|
|
996
|
+
// Every other platform gets neither and is refused outright.
|
|
885
997
|
function requireDescriptorAnchoring() {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
fs.
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
998
|
+
const directoryFlagsAvailable =
|
|
999
|
+
fs.constants.O_DIRECTORY !== undefined && fs.constants.O_NOFOLLOW !== undefined;
|
|
1000
|
+
if (process.platform === 'linux') {
|
|
1001
|
+
if (!directoryFlagsAvailable || !fs.existsSync('/proc/self/fd')) {
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
'Safe generated-plan writes require Linux /proc/self/fd and O_DIRECTORY/O_NOFOLLOW; refusing an unanchored write',
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
return;
|
|
895
1007
|
}
|
|
1008
|
+
if (process.platform === 'darwin') {
|
|
1009
|
+
if (!directoryFlagsAvailable) {
|
|
1010
|
+
throw new Error(
|
|
1011
|
+
'Safe generated-plan writes require macOS O_DIRECTORY/O_NOFOLLOW; refusing an unverified write',
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`Safe generated-plan writes require Linux /proc/self/fd or macOS O_DIRECTORY/O_NOFOLLOW; ${process.platform} offers neither, so refusing an unanchored write`,
|
|
1018
|
+
);
|
|
896
1019
|
}
|
|
897
1020
|
|
|
898
1021
|
function descriptorPath(fd, childName) {
|
|
@@ -900,157 +1023,352 @@ function descriptorPath(fd, childName) {
|
|
|
900
1023
|
return childName === undefined ? base : path.join(base, childName);
|
|
901
1024
|
}
|
|
902
1025
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
let directoryStats;
|
|
953
|
-
let executableStat;
|
|
1026
|
+
// Directory opens are plain O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC on both
|
|
1027
|
+
// platforms, and deliberately nothing else.
|
|
1028
|
+
//
|
|
1029
|
+
// O_NOFOLLOW_ANY (macOS 11+) used to be ORed in here on the theory that XNU
|
|
1030
|
+
// ignores unrecognized open flag bits, so it would be inert where unsupported.
|
|
1031
|
+
// That was wrong: combined with O_DIRECTORY macOS rejects it outright with
|
|
1032
|
+
// EINVAL, and every directory open on Darwin failed. It is gone and is not
|
|
1033
|
+
// coming back behind a probe or a degrade-on-EINVAL path — the per-component
|
|
1034
|
+
// O_NOFOLLOW walk is what delivers the guarantee. Rust's cap-std, the closest
|
|
1035
|
+
// reference implementation of this problem, has not adopted O_NOFOLLOW_ANY
|
|
1036
|
+
// either (their issue #179 is still open).
|
|
1037
|
+
function openVerifiedDirectory(absolute, flags) {
|
|
1038
|
+
return fs.openSync(absolute, flags);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// File opens additionally get O_NONBLOCK, which directory opens do not need:
|
|
1042
|
+
// it stops a FIFO swapped in at the target name from wedging the process on
|
|
1043
|
+
// open. The identity comparison that follows rejects the FIFO anyway, but only
|
|
1044
|
+
// if we ever get as far as running it.
|
|
1045
|
+
function openVerifiedFile(absolute, flags, mode) {
|
|
1046
|
+
const nonBlocking = flags | (fs.constants.O_NONBLOCK ?? 0);
|
|
1047
|
+
return mode === undefined
|
|
1048
|
+
? fs.openSync(absolute, nonBlocking)
|
|
1049
|
+
: fs.openSync(absolute, nonBlocking, mode);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// The publish primitive, identical on both platforms.
|
|
1053
|
+
//
|
|
1054
|
+
// link() is the portable no-replace publish: it fails with EEXIST if the
|
|
1055
|
+
// destination name is taken — by a regular file, by a directory, or by a symlink,
|
|
1056
|
+
// live or dangling — and it never follows that symlink to clobber its target.
|
|
1057
|
+
// It also works where renameat2(RENAME_NOREPLACE) does not, notably v9fs, which
|
|
1058
|
+
// is why the WSL2 9p case that used to fail every time now works.
|
|
1059
|
+
//
|
|
1060
|
+
// The published file is the same inode as the temporary, so every identity
|
|
1061
|
+
// comparison the callers already make still holds, and validateCommittedPlan
|
|
1062
|
+
// becomes strictly stronger: it compares the destination against the exact inode
|
|
1063
|
+
// whose bytes were fsynced.
|
|
1064
|
+
//
|
|
1065
|
+
// On Linux both paths are /proc/self/fd/<fd>/<name>, so the publish is anchored
|
|
1066
|
+
// to the held parent descriptors exactly like every other operation.
|
|
1067
|
+
// link(2) BUGS: "On NFS filesystems, the return code may be wrong in case the NFS
|
|
1068
|
+
// server performs the link creation and dies before it can say so. Use stat(2) to
|
|
1069
|
+
// find out if the link got created." open(2) NOTES gives the remedy this
|
|
1070
|
+
// implements: on a reported failure, stat the source and see whether its link
|
|
1071
|
+
// count reached 2. A false positive would need someone to have hardlinked a
|
|
1072
|
+
// 16-random-byte name inside a directory we hold open — and validateCommittedPlan
|
|
1073
|
+
// still proves the destination is the exact temporary inode afterwards.
|
|
1074
|
+
function linkCreatedDespiteError(sourcePath) {
|
|
954
1075
|
try {
|
|
955
|
-
|
|
956
|
-
resolved = fs.realpathSync(candidate);
|
|
957
|
-
const resolvedExecutableDirectory = fs.realpathSync(path.dirname(resolved));
|
|
958
|
-
directoryStats = [...new Set([resolvedDirectory, resolvedExecutableDirectory])].map(
|
|
959
|
-
(directory) => fs.statSync(directory),
|
|
960
|
-
);
|
|
961
|
-
executableStat = fs.lstatSync(resolved);
|
|
962
|
-
fs.accessSync(resolved, fs.constants.X_OK);
|
|
1076
|
+
return fs.statSync(sourcePath, { bigint: true }).nlink === 2n;
|
|
963
1077
|
} catch {
|
|
964
|
-
return
|
|
1078
|
+
return false;
|
|
965
1079
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function linkNoReplace(sourcePath, destinationPath) {
|
|
1083
|
+
try {
|
|
1084
|
+
fs.linkSync(sourcePath, destinationPath);
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
// Callers treat "destination taken" as a distinct outcome, not a failure.
|
|
1087
|
+
if (error?.code === 'EEXIST') return false;
|
|
1088
|
+
if (!linkCreatedDespiteError(sourcePath)) {
|
|
1089
|
+
// FAT, Coda, and some SMB/FUSE/virtiofs mounts have no hardlinks at all.
|
|
1090
|
+
// Git falls back to rename here, but git can afford to lose collision
|
|
1091
|
+
// detection because its objects are content-addressed; a plan destination
|
|
1092
|
+
// is a plain name, so a replacing rename would silently clobber whatever
|
|
1093
|
+
// is already there. Refuse loudly instead.
|
|
1094
|
+
if (error?.code === 'EPERM' || error?.code === 'ENOTSUP' || error?.code === 'EMLINK') {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`Generated-plan publication requires hard links, which this filesystem refused (${error.code}); refusing to fall back to a replacing rename`,
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
throw error;
|
|
1100
|
+
}
|
|
972
1101
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
) {
|
|
980
|
-
return null;
|
|
1102
|
+
try {
|
|
1103
|
+
fs.unlinkSync(sourcePath);
|
|
1104
|
+
} catch {
|
|
1105
|
+
// The link succeeded, so the plan IS published. A temporary name left behind
|
|
1106
|
+
// is a stray file, not an unpublished plan: reporting it as a failure would
|
|
1107
|
+
// be a lie, and rolling back would unpublish a plan that is already live.
|
|
981
1108
|
}
|
|
982
|
-
return
|
|
1109
|
+
return true;
|
|
983
1110
|
}
|
|
984
1111
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1112
|
+
// A directory holder is anything that owns a verified chain: a plan-parent
|
|
1113
|
+
// handle, a ref's parent directory, or an absence guard. Two arrays describe it,
|
|
1114
|
+
// both root-first and the same length — `chain` records each element's expected
|
|
1115
|
+
// path and dev/ino/mode, and `descriptors` holds an open descriptor on each.
|
|
1116
|
+
//
|
|
1117
|
+
// Holding those descriptors is load-bearing rather than decorative. dev/ino/mode
|
|
1118
|
+
// is unique only among *live* inodes: an inode number freed by an rmdir is handed
|
|
1119
|
+
// straight back to the next mkdir, so a replacement directory can reproduce a
|
|
1120
|
+
// recorded identity exactly. An open descriptor pins the inode, so the number
|
|
1121
|
+
// cannot be recycled for as long as the holder exists.
|
|
1122
|
+
function verifyPinnedDescriptors(holder) {
|
|
1123
|
+
const { chain, descriptors } = holder;
|
|
1124
|
+
if (!Array.isArray(descriptors) || descriptors.length !== chain.length) {
|
|
1125
|
+
throw new Error('Generated-plan parent chain is missing the descriptors that pin it');
|
|
1126
|
+
}
|
|
1127
|
+
chain.forEach((item, index) => {
|
|
1128
|
+
const pinned = fs.fstatSync(descriptors[index], { bigint: true });
|
|
1129
|
+
if (!pinned.isDirectory() || stableDirectoryIdentity(pinned) !== item.identity) {
|
|
1130
|
+
throw new Error('Generated-plan parent descriptor changed during the write');
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function verifyLexicalChain(holder) {
|
|
1136
|
+
for (const item of holder.chain) {
|
|
1137
|
+
let lexical;
|
|
1138
|
+
try {
|
|
1139
|
+
lexical = fs.lstatSync(item.expectedPath, { bigint: true });
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error;
|
|
1142
|
+
// A parent renamed out from under us is a mismatch, not a missing file:
|
|
1143
|
+
// reporting the raw ENOENT would leak an unrelated-looking error out of a
|
|
1144
|
+
// check whose whole job is to say the chain no longer holds.
|
|
1145
|
+
throw new Error('Generated-plan lexical parent no longer matches its directory descriptor');
|
|
1146
|
+
}
|
|
1147
|
+
if (
|
|
1148
|
+
lexical.isSymbolicLink() ||
|
|
1149
|
+
!lexical.isDirectory() ||
|
|
1150
|
+
stableDirectoryIdentity(lexical) !== item.identity
|
|
1151
|
+
) {
|
|
1152
|
+
throw new Error('Generated-plan lexical parent no longer matches its directory descriptor');
|
|
1153
|
+
}
|
|
993
1154
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// The whole platform seam, in five methods. Everything else an operation does is
|
|
1158
|
+
// identical on both platforms and lives in the shared functions below.
|
|
1159
|
+
//
|
|
1160
|
+
// Only two things actually differ: how a name becomes a path, and what guard
|
|
1161
|
+
// wraps the operation that uses it.
|
|
1162
|
+
//
|
|
1163
|
+
// Linux ANCHORS. /proc/self/fd/<fd>/<name> starts the walk at the inode the
|
|
1164
|
+
// descriptor holds, so a parent renamed away cannot be traversed at all and the
|
|
1165
|
+
// guard is a no-op — there is nothing left to verify.
|
|
1166
|
+
//
|
|
1167
|
+
// macOS VERIFIES. It resolves lexically, so before and after every operation it
|
|
1168
|
+
// proves that each element of the path chain still names the exact inode being
|
|
1169
|
+
// held for it. That DETECTS a swapped parent and aborts; it does not make the
|
|
1170
|
+
// swap impossible. A swap landing inside the window is caught by the trailing
|
|
1171
|
+
// check, after the fact, rather than being unreachable. The check runs after a
|
|
1172
|
+
// failure too, because a verdict observed through a chain that has since changed
|
|
1173
|
+
// is not a verdict.
|
|
1174
|
+
const LINUX_ANCHORING = {
|
|
1175
|
+
childPath(dirHandle, childName) {
|
|
1176
|
+
return descriptorPath(dirHandle.fd, childName);
|
|
1177
|
+
},
|
|
1178
|
+
verified(holders, run) {
|
|
1179
|
+
return run();
|
|
1180
|
+
},
|
|
1181
|
+
descriptorMatchesChild(fd, expectedPath) {
|
|
1182
|
+
return fs.realpathSync.native(descriptorPath(fd)) === expectedPath;
|
|
1183
|
+
},
|
|
1184
|
+
parentStillResolves(parentHandle) {
|
|
1185
|
+
return fs.realpathSync.native(descriptorPath(parentHandle.fd)) === parentHandle.expectedPath;
|
|
1186
|
+
},
|
|
1187
|
+
verifyAbsentChild(guard) {
|
|
1188
|
+
if (absentChildIsPresent(guard.ref)) {
|
|
1189
|
+
throw new Error(`${guard.repoPath} appeared before evidence materialization completed`);
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
const DARWIN_ANCHORING = {
|
|
1195
|
+
childPath(dirHandle, childName) {
|
|
1196
|
+
return path.join(dirHandle.expectedPath, childName);
|
|
1197
|
+
},
|
|
1198
|
+
verified(holders, run) {
|
|
1199
|
+
const list = Array.isArray(holders) ? holders : [holders];
|
|
1200
|
+
const proveChain = () => {
|
|
1201
|
+
for (const holder of list) {
|
|
1202
|
+
verifyPinnedDescriptors(holder);
|
|
1203
|
+
verifyLexicalChain(holder);
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
proveChain();
|
|
1207
|
+
let value;
|
|
998
1208
|
try {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
} catch {
|
|
1004
|
-
continue;
|
|
1209
|
+
value = run();
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
proveChain();
|
|
1212
|
+
throw error;
|
|
1005
1213
|
}
|
|
1214
|
+
proveChain();
|
|
1215
|
+
return value;
|
|
1216
|
+
},
|
|
1217
|
+
descriptorMatchesChild(fd, _expectedPath, childStat) {
|
|
1218
|
+
// There is no live fd-to-path oracle on macOS (F_GETPATH is a name-cache
|
|
1219
|
+
// snapshot, not an anchor), so escape is decided the other way round: the
|
|
1220
|
+
// name was just resolved under a verified chain, and the descriptor opened
|
|
1221
|
+
// from it counts only if it is that same inode.
|
|
1006
1222
|
const opened = fs.fstatSync(fd, { bigint: true });
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
executable,
|
|
1010
|
-
['-I', '-S', '-c', 'import sys; print(sys.version_info[0])'],
|
|
1011
|
-
{
|
|
1012
|
-
encoding: 'utf8',
|
|
1013
|
-
env: { ...process.env, LANG: 'C', LC_ALL: 'C' },
|
|
1014
|
-
timeout: 10_000,
|
|
1015
|
-
windowsHide: true,
|
|
1016
|
-
},
|
|
1223
|
+
return (
|
|
1224
|
+
opened.isDirectory() && stableDirectoryIdentity(opened) === stableDirectoryIdentity(childStat)
|
|
1017
1225
|
);
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1226
|
+
},
|
|
1227
|
+
parentStillResolves(parentHandle) {
|
|
1228
|
+
// Both halves are needed: a directory renamed away keeps its inode, so the
|
|
1229
|
+
// descriptors alone still match and only the lexical half notices it moved.
|
|
1230
|
+
try {
|
|
1231
|
+
verifyPinnedDescriptors(parentHandle);
|
|
1232
|
+
verifyLexicalChain(parentHandle);
|
|
1233
|
+
} catch {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
return true;
|
|
1237
|
+
},
|
|
1238
|
+
verifyAbsentChild(guard) {
|
|
1239
|
+
let present;
|
|
1240
|
+
try {
|
|
1241
|
+
present = DARWIN_ANCHORING.verified(guard.handle, () => absentChildIsPresent(guard.ref));
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
// A chain that no longer holds makes the absence verdict meaningless, and
|
|
1244
|
+
// the caller reports that as the anchor changing rather than as a stray
|
|
1245
|
+
// parent-descriptor error. Linux cannot reach this: its guard is a no-op.
|
|
1246
|
+
throw new Error(
|
|
1247
|
+
`Absence anchor changed for ${guard.repoPath}: ${
|
|
1248
|
+
error instanceof Error ? error.message : String(error)
|
|
1249
|
+
}`,
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
if (present) {
|
|
1253
|
+
throw new Error(`${guard.repoPath} appeared before evidence materialization completed`);
|
|
1021
1254
|
}
|
|
1255
|
+
},
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
const ANCHORING_BACKENDS = new Map([
|
|
1259
|
+
['linux', LINUX_ANCHORING],
|
|
1260
|
+
['darwin', DARWIN_ANCHORING],
|
|
1261
|
+
]);
|
|
1262
|
+
|
|
1263
|
+
function anchoringBackend() {
|
|
1264
|
+
const backend = ANCHORING_BACKENDS.get(process.platform);
|
|
1265
|
+
if (!backend) {
|
|
1266
|
+
// requireDescriptorAnchoring normally refuses first; this is the same answer
|
|
1267
|
+
// from the other side, so an unsupported platform can never fall through to
|
|
1268
|
+
// whichever backend happened to be the ternary's default.
|
|
1269
|
+
throw new Error(
|
|
1270
|
+
`No generated-plan anchoring backend for ${process.platform}; refusing an unanchored write`,
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
return backend;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Open, fstat, compare, close on mismatch. The descriptor never escapes this
|
|
1277
|
+
// function unless it refers to the inode the caller already verified by name, so
|
|
1278
|
+
// a lexical open that landed anywhere else cannot be used by accident. On Linux
|
|
1279
|
+
// the comparison passes trivially — the /proc walk already resolved from the
|
|
1280
|
+
// held parent — and costs one fstat to keep the guarantee structural rather than
|
|
1281
|
+
// dependent on which backend is in play.
|
|
1282
|
+
function adoptVerifiedFile(ref, expectedStat, flags) {
|
|
1283
|
+
const fd = openVerifiedFile(ref.path, flags);
|
|
1284
|
+
let opened;
|
|
1285
|
+
try {
|
|
1286
|
+
opened = fs.fstatSync(fd, { bigint: true });
|
|
1287
|
+
} catch (error) {
|
|
1022
1288
|
fs.closeSync(fd);
|
|
1289
|
+
throw error;
|
|
1023
1290
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1291
|
+
if (stableFileIdentity(opened) !== stableFileIdentity(expectedStat)) {
|
|
1292
|
+
fs.closeSync(fd);
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
return fd;
|
|
1027
1296
|
}
|
|
1028
1297
|
|
|
1029
|
-
function
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
encoding: 'utf8',
|
|
1036
|
-
env: { ...process.env, LANG: 'C', LC_ALL: 'C' },
|
|
1037
|
-
timeout: 10_000,
|
|
1038
|
-
windowsHide: true,
|
|
1039
|
-
},
|
|
1040
|
-
);
|
|
1041
|
-
if (result.error) throw result.error;
|
|
1042
|
-
if (result.status === 17) return false;
|
|
1043
|
-
if (result.status !== 0) {
|
|
1044
|
-
throw new Error(
|
|
1045
|
-
`Atomic no-replace move failed (${result.status}): ${(result.stderr ?? '').trim()}`,
|
|
1046
|
-
);
|
|
1298
|
+
function absentChildIsPresent(ref) {
|
|
1299
|
+
try {
|
|
1300
|
+
fs.lstatSync(ref.path, { bigint: true });
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
if (error?.code === 'ENOENT') return false;
|
|
1303
|
+
throw error;
|
|
1047
1304
|
}
|
|
1048
1305
|
return true;
|
|
1049
1306
|
}
|
|
1050
1307
|
|
|
1051
|
-
|
|
1308
|
+
// The operations. Each is the same on both platforms; only the guard differs.
|
|
1309
|
+
function lstatChild(ref) {
|
|
1310
|
+
return anchoringBackend().verified(ref.dir, () => fs.lstatSync(ref.path, { bigint: true }));
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function openChildRead(ref, flags, expectedStat) {
|
|
1314
|
+
return anchoringBackend().verified(ref.dir, () => {
|
|
1315
|
+
const fd = adoptVerifiedFile(ref, expectedStat, flags);
|
|
1316
|
+
if (fd === null) {
|
|
1317
|
+
throw new Error(`${ref.name} was replaced between its verified stat and its no-follow open`);
|
|
1318
|
+
}
|
|
1319
|
+
return fd;
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function createChild(ref, flags, mode) {
|
|
1324
|
+
// O_CREAT|O_EXCL|O_NOFOLLOW is atomic at the leaf, so the only thing the guard
|
|
1325
|
+
// has to cover is which directory the leaf landed in.
|
|
1326
|
+
return anchoringBackend().verified(ref.dir, () => openVerifiedFile(ref.path, flags, mode));
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
function mkdirChild(ref, mode) {
|
|
1330
|
+
anchoringBackend().verified(ref.dir, () => fs.mkdirSync(ref.path, { mode }));
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function publishNoReplace(sourceRef, destinationRef) {
|
|
1334
|
+
return anchoringBackend().verified([sourceRef.dir, destinationRef.dir], () =>
|
|
1335
|
+
linkNoReplace(sourceRef.path, destinationRef.path),
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// The single place a name becomes a path, and therefore the right place to
|
|
1340
|
+
// enforce that a name is one ordinary component.
|
|
1341
|
+
//
|
|
1342
|
+
// A trailing separator is the sharp edge here, not a tidiness concern:
|
|
1343
|
+
// open(path, O_NOFOLLOW) FOLLOWS a symlink when path ends in "/" — the trap
|
|
1344
|
+
// behind CVE-2026-39822 / golang/go#79005, which let os.Root escape its own
|
|
1345
|
+
// root. path.join preserves that trailing slash, so a component carrying one
|
|
1346
|
+
// would turn every no-follow open in this file into a following one.
|
|
1347
|
+
// normalizeRepoPath already rejects such components upstream; this is the
|
|
1348
|
+
// chokepoint that makes it true for every caller, including the generated
|
|
1349
|
+
// temporary and vault names that never pass through it.
|
|
1350
|
+
function anchoredChild(dirHandle, childName) {
|
|
1351
|
+
if (
|
|
1352
|
+
typeof childName !== 'string' ||
|
|
1353
|
+
childName === '' ||
|
|
1354
|
+
childName === '.' ||
|
|
1355
|
+
childName === '..' ||
|
|
1356
|
+
childName.includes('/') ||
|
|
1357
|
+
childName.includes('\\') ||
|
|
1358
|
+
childName.includes('\0')
|
|
1359
|
+
) {
|
|
1360
|
+
throw new Error(`Refusing to resolve ${JSON.stringify(childName)} as a single path component`);
|
|
1361
|
+
}
|
|
1362
|
+
return {
|
|
1363
|
+
dir: dirHandle,
|
|
1364
|
+
name: childName,
|
|
1365
|
+
path: anchoringBackend().childPath(dirHandle, childName),
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function lstatAnchoredOptional(ref) {
|
|
1052
1370
|
try {
|
|
1053
|
-
return
|
|
1371
|
+
return lstatChild(ref);
|
|
1054
1372
|
} catch (error) {
|
|
1055
1373
|
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return null;
|
|
1056
1374
|
throw error;
|
|
@@ -1063,39 +1381,37 @@ function openPlanParent(
|
|
|
1063
1381
|
{ createMissing = true, purpose = 'Generated-plan' } = {},
|
|
1064
1382
|
) {
|
|
1065
1383
|
requireDescriptorAnchoring();
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
fs.constants.O_DIRECTORY |
|
|
1069
|
-
fs.constants.O_NOFOLLOW |
|
|
1070
|
-
(fs.constants.O_CLOEXEC ?? 0);
|
|
1384
|
+
// Root-first and index-aligned with `chain`: verifyPinnedDescriptors relies on
|
|
1385
|
+
// that, and the descriptors are what pin each recorded inode against reuse.
|
|
1071
1386
|
const descriptors = [];
|
|
1072
1387
|
try {
|
|
1073
|
-
let currentFd =
|
|
1388
|
+
let currentFd = openVerifiedDirectory(repo, ANCHORED_DIRECTORY_FLAGS);
|
|
1074
1389
|
descriptors.push(currentFd);
|
|
1075
1390
|
const rootStat = fs.fstatSync(currentFd, { bigint: true });
|
|
1076
1391
|
const chain = [{ expectedPath: repo, identity: stableDirectoryIdentity(rootStat) }];
|
|
1392
|
+
let currentHandle = { fd: currentFd, expectedPath: repo, chain, descriptors };
|
|
1077
1393
|
const traversed = [];
|
|
1078
1394
|
for (const component of parentComponents) {
|
|
1079
1395
|
traversed.push(component);
|
|
1080
|
-
const
|
|
1396
|
+
const child = anchoredChild(currentHandle, component);
|
|
1081
1397
|
let childStat;
|
|
1082
1398
|
let created = false;
|
|
1083
1399
|
try {
|
|
1084
|
-
childStat =
|
|
1400
|
+
childStat = lstatChild(child);
|
|
1085
1401
|
} catch (error) {
|
|
1086
1402
|
if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') throw error;
|
|
1087
1403
|
if (!createMissing) {
|
|
1088
1404
|
throw new Error(`${purpose} parent does not exist: ${traversed.join('/')}`);
|
|
1089
1405
|
}
|
|
1090
|
-
|
|
1091
|
-
childStat =
|
|
1406
|
+
mkdirChild(child, 0o755);
|
|
1407
|
+
childStat = lstatChild(child);
|
|
1092
1408
|
created = true;
|
|
1093
1409
|
}
|
|
1094
1410
|
if (childStat.isSymbolicLink() || !childStat.isDirectory()) {
|
|
1095
1411
|
throw new Error(`${purpose} parent is not a real directory: ${traversed.join('/')}`);
|
|
1096
1412
|
}
|
|
1097
1413
|
const parentFd = currentFd;
|
|
1098
|
-
const childFd =
|
|
1414
|
+
const childFd = openVerifiedDirectory(child.path, ANCHORED_DIRECTORY_FLAGS);
|
|
1099
1415
|
descriptors.push(childFd);
|
|
1100
1416
|
currentFd = childFd;
|
|
1101
1417
|
if (created) {
|
|
@@ -1103,18 +1419,16 @@ function openPlanParent(
|
|
|
1103
1419
|
fs.fsyncSync(parentFd);
|
|
1104
1420
|
}
|
|
1105
1421
|
const expected = path.join(repo, ...traversed);
|
|
1106
|
-
|
|
1107
|
-
if (actual !== expected) {
|
|
1422
|
+
if (!anchoringBackend().descriptorMatchesChild(currentFd, expected, childStat)) {
|
|
1108
1423
|
throw new Error(`${purpose} parent escaped the repository: ${traversed.join('/')}`);
|
|
1109
1424
|
}
|
|
1110
1425
|
const openedStat = fs.fstatSync(currentFd, { bigint: true });
|
|
1111
1426
|
chain.push({ expectedPath: expected, identity: stableDirectoryIdentity(openedStat) });
|
|
1427
|
+
currentHandle = { fd: currentFd, expectedPath: expected, chain, descriptors };
|
|
1112
1428
|
}
|
|
1113
|
-
const stat = fs.fstatSync(currentFd, { bigint: true });
|
|
1114
1429
|
return {
|
|
1115
1430
|
descriptors,
|
|
1116
1431
|
fd: currentFd,
|
|
1117
|
-
identity: stableDirectoryIdentity(stat),
|
|
1118
1432
|
expectedPath: path.join(repo, ...parentComponents),
|
|
1119
1433
|
chain,
|
|
1120
1434
|
};
|
|
@@ -1134,9 +1448,16 @@ function closeDescriptors(descriptors) {
|
|
|
1134
1448
|
}
|
|
1135
1449
|
}
|
|
1136
1450
|
|
|
1451
|
+
// A handle's identity IS its chain leaf's identity. Storing it twice meant two
|
|
1452
|
+
// fstats a line apart and a re-stamp helper to keep them agreeing; deriving it
|
|
1453
|
+
// removes both.
|
|
1454
|
+
function handleIdentity(handle) {
|
|
1455
|
+
return handle.chain[handle.chain.length - 1].identity;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1137
1458
|
function resolveGitDirectory(repo) {
|
|
1138
1459
|
const result = git(repo, ['rev-parse', '--absolute-git-dir']);
|
|
1139
|
-
return fs.realpathSync(decodeUtf8(result.stdout, 'Git administrative directory').trim());
|
|
1460
|
+
return fs.realpathSync.native(decodeUtf8(result.stdout, 'Git administrative directory').trim());
|
|
1140
1461
|
}
|
|
1141
1462
|
|
|
1142
1463
|
function openBackupVault(repo, { createMissing = true } = {}) {
|
|
@@ -1147,9 +1468,12 @@ function openBackupVault(repo, { createMissing = true } = {}) {
|
|
|
1147
1468
|
});
|
|
1148
1469
|
fs.fchmodSync(handle.fd, 0o700);
|
|
1149
1470
|
fs.fsyncSync(handle.fd);
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1471
|
+
// mode is part of every directory identity, so hardening the vault changes the
|
|
1472
|
+
// identity the chain recorded for it; without this the next verification would
|
|
1473
|
+
// reject the directory it just hardened.
|
|
1474
|
+
handle.chain[handle.chain.length - 1].identity = stableDirectoryIdentity(
|
|
1475
|
+
fs.fstatSync(handle.fd, { bigint: true }),
|
|
1476
|
+
);
|
|
1153
1477
|
return { ...handle, gitDirectory };
|
|
1154
1478
|
}
|
|
1155
1479
|
|
|
@@ -1157,33 +1481,28 @@ function validatePlanParent(parentHandle) {
|
|
|
1157
1481
|
const descriptorStat = fs.fstatSync(parentHandle.fd, { bigint: true });
|
|
1158
1482
|
if (
|
|
1159
1483
|
!descriptorStat.isDirectory() ||
|
|
1160
|
-
stableDirectoryIdentity(descriptorStat) !== parentHandle
|
|
1484
|
+
stableDirectoryIdentity(descriptorStat) !== handleIdentity(parentHandle)
|
|
1161
1485
|
) {
|
|
1162
1486
|
throw new Error('Generated-plan parent descriptor changed during the write');
|
|
1163
1487
|
}
|
|
1164
|
-
|
|
1165
|
-
if (descriptorRealPath !== parentHandle.expectedPath) {
|
|
1488
|
+
if (!anchoringBackend().parentStillResolves(parentHandle)) {
|
|
1166
1489
|
throw new Error('Generated-plan parent moved or was replaced during the write');
|
|
1167
1490
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
) {
|
|
1175
|
-
throw new Error('Generated-plan lexical parent no longer matches its directory descriptor');
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1491
|
+
// Both halves come from the shared helpers rather than being restated here: an
|
|
1492
|
+
// earlier hand-copy of the lexical loop lost verifyLexicalChain's ENOENT/ENOTDIR
|
|
1493
|
+
// translation, so a renamed parent could surface a raw errno from a function
|
|
1494
|
+
// with a dozen call sites.
|
|
1495
|
+
verifyPinnedDescriptors(parentHandle);
|
|
1496
|
+
verifyLexicalChain(parentHandle);
|
|
1178
1497
|
}
|
|
1179
1498
|
|
|
1180
1499
|
function inspectPlanDestination(
|
|
1181
|
-
|
|
1500
|
+
finalRef,
|
|
1182
1501
|
{ replace, expectedIdentity, mustBeAbsent = false } = {},
|
|
1183
1502
|
) {
|
|
1184
1503
|
let stat;
|
|
1185
1504
|
try {
|
|
1186
|
-
stat =
|
|
1505
|
+
stat = lstatChild(finalRef);
|
|
1187
1506
|
} catch (error) {
|
|
1188
1507
|
if (error?.code === 'ENOENT') {
|
|
1189
1508
|
if (expectedIdentity) throw new Error('Generated plan disappeared during the write');
|
|
@@ -1201,19 +1520,17 @@ function inspectPlanDestination(
|
|
|
1201
1520
|
if (expectedIdentity && identity !== expectedIdentity) {
|
|
1202
1521
|
throw new Error('Generated plan changed during the write');
|
|
1203
1522
|
}
|
|
1204
|
-
return
|
|
1523
|
+
return stat;
|
|
1205
1524
|
}
|
|
1206
1525
|
|
|
1207
|
-
function openExistingPlanDestination(
|
|
1208
|
-
const
|
|
1209
|
-
if (
|
|
1526
|
+
function openExistingPlanDestination(finalRef, replace) {
|
|
1527
|
+
const stat = inspectPlanDestination(finalRef, { replace });
|
|
1528
|
+
if (stat === null) {
|
|
1210
1529
|
if (replace) throw new Error('Deepen mode requires an existing generated plan to replace');
|
|
1211
1530
|
return { fd: undefined, identity: null, stableIdentity: null };
|
|
1212
1531
|
}
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0),
|
|
1216
|
-
);
|
|
1532
|
+
const identity = statIdentity(stat);
|
|
1533
|
+
const fd = openChildRead(finalRef, VERIFIED_READ_FLAGS, stat);
|
|
1217
1534
|
try {
|
|
1218
1535
|
const opened = fs.fstatSync(fd, { bigint: true });
|
|
1219
1536
|
if (!opened.isFile() || statIdentity(opened) !== identity) {
|
|
@@ -1264,8 +1581,8 @@ function hashOpenFile(fd, label) {
|
|
|
1264
1581
|
};
|
|
1265
1582
|
}
|
|
1266
1583
|
|
|
1267
|
-
function validateCommittedPlan(
|
|
1268
|
-
const before =
|
|
1584
|
+
function validateCommittedPlan(finalRef, tempFd, expectedTemp, testHooks) {
|
|
1585
|
+
const before = lstatChild(finalRef);
|
|
1269
1586
|
if (
|
|
1270
1587
|
before.isSymbolicLink() ||
|
|
1271
1588
|
!before.isFile() ||
|
|
@@ -1273,19 +1590,16 @@ function validateCommittedPlan(finalPath, tempFd, expectedTemp, testHooks) {
|
|
|
1273
1590
|
) {
|
|
1274
1591
|
throw new Error('Generated-plan destination failed its first post-write identity check');
|
|
1275
1592
|
}
|
|
1276
|
-
const finalFd =
|
|
1277
|
-
finalPath,
|
|
1278
|
-
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0),
|
|
1279
|
-
);
|
|
1593
|
+
const finalFd = openChildRead(finalRef, VERIFIED_READ_FLAGS, before);
|
|
1280
1594
|
try {
|
|
1281
1595
|
const opened = fs.fstatSync(finalFd, { bigint: true });
|
|
1282
1596
|
if (!opened.isFile() || stableFileIdentity(opened) !== expectedTemp.identity) {
|
|
1283
1597
|
throw new Error('Generated-plan destination changed while its no-follow descriptor opened');
|
|
1284
1598
|
}
|
|
1285
|
-
testHooks?.afterFinalOpen?.({ fd: finalFd, finalPath });
|
|
1599
|
+
testHooks?.afterFinalOpen?.({ fd: finalFd, finalPath: finalRef.path });
|
|
1286
1600
|
const committedViaTemp = hashOpenFile(tempFd, 'generated-plan committed file');
|
|
1287
1601
|
const committedViaPath = hashOpenFile(finalFd, 'generated-plan destination descriptor');
|
|
1288
|
-
const after =
|
|
1602
|
+
const after = lstatChild(finalRef);
|
|
1289
1603
|
const openedAfter = fs.fstatSync(finalFd, { bigint: true });
|
|
1290
1604
|
if (
|
|
1291
1605
|
after.isSymbolicLink() ||
|
|
@@ -1320,22 +1634,19 @@ function copyOpenFile(sourceFd, destinationFd, label) {
|
|
|
1320
1634
|
return after;
|
|
1321
1635
|
}
|
|
1322
1636
|
|
|
1323
|
-
function
|
|
1324
|
-
const before =
|
|
1637
|
+
function openVerifiedAnchoredFile(ref, label, knownStat) {
|
|
1638
|
+
const before = knownStat ?? lstatChild(ref);
|
|
1325
1639
|
if (before.isSymbolicLink() || !before.isFile()) {
|
|
1326
1640
|
throw new Error(`${label} is not a regular no-follow file`);
|
|
1327
1641
|
}
|
|
1328
|
-
const fd =
|
|
1329
|
-
absolute,
|
|
1330
|
-
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0),
|
|
1331
|
-
);
|
|
1642
|
+
const fd = openChildRead(ref, VERIFIED_READ_FLAGS, before);
|
|
1332
1643
|
try {
|
|
1333
1644
|
const opened = fs.fstatSync(fd, { bigint: true });
|
|
1334
1645
|
if (!opened.isFile() || stableFileIdentity(opened) !== stableFileIdentity(before)) {
|
|
1335
1646
|
throw new Error(`${label} changed while its descriptor opened`);
|
|
1336
1647
|
}
|
|
1337
1648
|
const layer = hashOpenFile(fd, label);
|
|
1338
|
-
const after =
|
|
1649
|
+
const after = lstatChild(ref);
|
|
1339
1650
|
if (after.isSymbolicLink() || !after.isFile() || stableFileIdentity(after) !== layer.identity) {
|
|
1340
1651
|
throw new Error(`${label} changed after verification`);
|
|
1341
1652
|
}
|
|
@@ -1358,10 +1669,10 @@ export function readPlanSafely({ repo: repoInput, generatedPlanPath, testHooks }
|
|
|
1358
1669
|
let fd;
|
|
1359
1670
|
try {
|
|
1360
1671
|
validatePlanParent(parentHandle);
|
|
1361
|
-
const
|
|
1672
|
+
const finalRef = anchoredChild(parentHandle, finalName);
|
|
1362
1673
|
let before;
|
|
1363
1674
|
try {
|
|
1364
|
-
before =
|
|
1675
|
+
before = lstatChild(finalRef);
|
|
1365
1676
|
} catch (error) {
|
|
1366
1677
|
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
1367
1678
|
throw new Error(`Loaded plan does not exist: ${generatedPlan}`);
|
|
@@ -1371,15 +1682,12 @@ export function readPlanSafely({ repo: repoInput, generatedPlanPath, testHooks }
|
|
|
1371
1682
|
if (before.isSymbolicLink() || !before.isFile()) {
|
|
1372
1683
|
throw new Error('Loaded plan must be a regular file, never a symlink');
|
|
1373
1684
|
}
|
|
1374
|
-
fd =
|
|
1375
|
-
finalPath,
|
|
1376
|
-
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | (fs.constants.O_CLOEXEC ?? 0),
|
|
1377
|
-
);
|
|
1685
|
+
fd = openChildRead(finalRef, VERIFIED_READ_FLAGS, before);
|
|
1378
1686
|
const opened = fs.fstatSync(fd, { bigint: true });
|
|
1379
1687
|
if (!opened.isFile() || statIdentity(opened) !== statIdentity(before)) {
|
|
1380
1688
|
throw new Error('Loaded plan changed while its no-follow descriptor opened');
|
|
1381
1689
|
}
|
|
1382
|
-
testHooks?.afterPlanOpen?.({ fd, finalPath });
|
|
1690
|
+
testHooks?.afterPlanOpen?.({ fd, finalPath: finalRef.path });
|
|
1383
1691
|
const chunks = [];
|
|
1384
1692
|
let total = 0;
|
|
1385
1693
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
@@ -1394,7 +1702,7 @@ export function readPlanSafely({ repo: repoInput, generatedPlanPath, testHooks }
|
|
|
1394
1702
|
decodeUtf8(contents, 'loaded plan');
|
|
1395
1703
|
const after = fs.fstatSync(fd, { bigint: true });
|
|
1396
1704
|
assertStableIdentity(opened, after, 'loaded plan');
|
|
1397
|
-
const pathAfter =
|
|
1705
|
+
const pathAfter = lstatChild(finalRef);
|
|
1398
1706
|
if (
|
|
1399
1707
|
pathAfter.isSymbolicLink() ||
|
|
1400
1708
|
!pathAfter.isFile() ||
|
|
@@ -1419,24 +1727,22 @@ function artifactGitPath(name) {
|
|
|
1419
1727
|
return `gitnexus-plan-backups/${name}`;
|
|
1420
1728
|
}
|
|
1421
1729
|
|
|
1422
|
-
function verifyVaultArtifactFromFreshRoot(repo,
|
|
1423
|
-
const components = gitPath.split('/');
|
|
1424
|
-
if (components.length !== 2 || components[0] !== 'gitnexus-plan-backups') {
|
|
1425
|
-
throw new Error(`Invalid Git-admin artifact path: ${gitPath}`);
|
|
1426
|
-
}
|
|
1730
|
+
function verifyVaultArtifactFromFreshRoot(repo, name, expectedLayer) {
|
|
1427
1731
|
const freshVault = openBackupVault(repo, { createMissing: false });
|
|
1428
1732
|
try {
|
|
1429
1733
|
validatePlanParent(freshVault);
|
|
1430
|
-
const opened =
|
|
1431
|
-
|
|
1432
|
-
`Git-admin artifact ${
|
|
1734
|
+
const opened = openVerifiedAnchoredFile(
|
|
1735
|
+
anchoredChild(freshVault, name),
|
|
1736
|
+
`Git-admin artifact ${artifactGitPath(name)}`,
|
|
1433
1737
|
);
|
|
1434
1738
|
try {
|
|
1435
1739
|
if (
|
|
1436
1740
|
opened.layer.identity !== expectedLayer.identity ||
|
|
1437
1741
|
opened.layer.digest !== expectedLayer.digest
|
|
1438
1742
|
) {
|
|
1439
|
-
throw new Error(
|
|
1743
|
+
throw new Error(
|
|
1744
|
+
`Git-admin artifact changed before fresh-root verification: ${artifactGitPath(name)}`,
|
|
1745
|
+
);
|
|
1440
1746
|
}
|
|
1441
1747
|
} finally {
|
|
1442
1748
|
fs.closeSync(opened.fd);
|
|
@@ -1449,16 +1755,8 @@ function verifyVaultArtifactFromFreshRoot(repo, gitPath, expectedLayer) {
|
|
|
1449
1755
|
function createVaultCopyFromFd(repo, vault, sourceFd, role) {
|
|
1450
1756
|
validatePlanParent(vault);
|
|
1451
1757
|
const name = `.gitnexus-plan-${role}-${process.pid}-${randomBytes(16).toString('hex')}.bak`;
|
|
1452
|
-
const
|
|
1453
|
-
const destinationFd =
|
|
1454
|
-
absolute,
|
|
1455
|
-
fs.constants.O_RDWR |
|
|
1456
|
-
fs.constants.O_CREAT |
|
|
1457
|
-
fs.constants.O_EXCL |
|
|
1458
|
-
fs.constants.O_NOFOLLOW |
|
|
1459
|
-
(fs.constants.O_CLOEXEC ?? 0),
|
|
1460
|
-
0o600,
|
|
1461
|
-
);
|
|
1758
|
+
const artifact = anchoredChild(vault, name);
|
|
1759
|
+
const destinationFd = createChild(artifact, VERIFIED_CREATE_FLAGS, 0o600);
|
|
1462
1760
|
let destination;
|
|
1463
1761
|
try {
|
|
1464
1762
|
const sourceStat = copyOpenFile(sourceFd, destinationFd, role);
|
|
@@ -1469,7 +1767,7 @@ function createVaultCopyFromFd(repo, vault, sourceFd, role) {
|
|
|
1469
1767
|
if (source.size !== destination.size || source.digest !== destination.digest) {
|
|
1470
1768
|
throw new Error(`${role} vault copy does not match its held source descriptor`);
|
|
1471
1769
|
}
|
|
1472
|
-
const pathStat =
|
|
1770
|
+
const pathStat = lstatChild(artifact);
|
|
1473
1771
|
if (
|
|
1474
1772
|
pathStat.isSymbolicLink() ||
|
|
1475
1773
|
!pathStat.isFile() ||
|
|
@@ -1481,24 +1779,15 @@ function createVaultCopyFromFd(repo, vault, sourceFd, role) {
|
|
|
1481
1779
|
} finally {
|
|
1482
1780
|
fs.closeSync(destinationFd);
|
|
1483
1781
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
return { role, gitPath, layer: destination };
|
|
1782
|
+
verifyVaultArtifactFromFreshRoot(repo, name, destination);
|
|
1783
|
+
return { role, gitPath: artifactGitPath(name), layer: destination };
|
|
1487
1784
|
}
|
|
1488
1785
|
|
|
1489
1786
|
function createVaultCopyFromBytes(repo, vault, contents, role) {
|
|
1490
1787
|
validatePlanParent(vault);
|
|
1491
1788
|
const name = `.gitnexus-plan-${role}-${process.pid}-${randomBytes(16).toString('hex')}.bak`;
|
|
1492
|
-
const
|
|
1493
|
-
const fd =
|
|
1494
|
-
absolute,
|
|
1495
|
-
fs.constants.O_RDWR |
|
|
1496
|
-
fs.constants.O_CREAT |
|
|
1497
|
-
fs.constants.O_EXCL |
|
|
1498
|
-
fs.constants.O_NOFOLLOW |
|
|
1499
|
-
(fs.constants.O_CLOEXEC ?? 0),
|
|
1500
|
-
0o600,
|
|
1501
|
-
);
|
|
1789
|
+
const artifact = anchoredChild(vault, name);
|
|
1790
|
+
const fd = createChild(artifact, VERIFIED_CREATE_FLAGS, 0o600);
|
|
1502
1791
|
let layer;
|
|
1503
1792
|
try {
|
|
1504
1793
|
writeAll(fd, contents);
|
|
@@ -1508,7 +1797,7 @@ function createVaultCopyFromBytes(repo, vault, contents, role) {
|
|
|
1508
1797
|
if (layer.size !== BigInt(contents.length) || layer.digest !== sha256(contents)) {
|
|
1509
1798
|
throw new Error(`${role} vault copy does not match the intended plan bytes`);
|
|
1510
1799
|
}
|
|
1511
|
-
const pathStat =
|
|
1800
|
+
const pathStat = lstatChild(artifact);
|
|
1512
1801
|
if (
|
|
1513
1802
|
pathStat.isSymbolicLink() ||
|
|
1514
1803
|
!pathStat.isFile() ||
|
|
@@ -1520,32 +1809,31 @@ function createVaultCopyFromBytes(repo, vault, contents, role) {
|
|
|
1520
1809
|
} finally {
|
|
1521
1810
|
fs.closeSync(fd);
|
|
1522
1811
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
return { role, gitPath, layer };
|
|
1812
|
+
verifyVaultArtifactFromFreshRoot(repo, name, layer);
|
|
1813
|
+
return { role, gitPath: artifactGitPath(name), layer };
|
|
1526
1814
|
}
|
|
1527
1815
|
|
|
1528
1816
|
function movePathToVault(repo, sourceHandle, sourceName, vault, role) {
|
|
1529
|
-
const source =
|
|
1530
|
-
if (!
|
|
1817
|
+
const source = anchoredChild(sourceHandle, sourceName);
|
|
1818
|
+
if (!lstatAnchoredOptional(source)) return null;
|
|
1531
1819
|
const name = `.gitnexus-plan-${role}-${process.pid}-${randomBytes(16).toString('hex')}.bak`;
|
|
1532
|
-
const destination =
|
|
1533
|
-
const moved =
|
|
1534
|
-
externalDescriptorPath(sourceHandle.fd, sourceName),
|
|
1535
|
-
externalDescriptorPath(vault.fd, name),
|
|
1536
|
-
);
|
|
1820
|
+
const destination = anchoredChild(vault, name);
|
|
1821
|
+
const moved = publishNoReplace(source, destination);
|
|
1537
1822
|
if (!moved) throw new Error(`${role} preservation destination unexpectedly exists`);
|
|
1538
1823
|
fs.fsyncSync(sourceHandle.fd);
|
|
1539
1824
|
if (vault.fd !== sourceHandle.fd) fs.fsyncSync(vault.fd);
|
|
1540
|
-
const sourceAfter =
|
|
1541
|
-
const destinationAfter =
|
|
1825
|
+
const sourceAfter = lstatAnchoredOptional(source);
|
|
1826
|
+
const destinationAfter = lstatAnchoredOptional(destination);
|
|
1542
1827
|
if (sourceAfter || !destinationAfter) {
|
|
1543
1828
|
throw new Error(`${role} could not be atomically moved into the Git-admin vault`);
|
|
1544
1829
|
}
|
|
1545
|
-
const opened =
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1830
|
+
const opened = openVerifiedAnchoredFile(
|
|
1831
|
+
destination,
|
|
1832
|
+
`${role} Git-admin artifact`,
|
|
1833
|
+
destinationAfter,
|
|
1834
|
+
);
|
|
1835
|
+
verifyVaultArtifactFromFreshRoot(repo, name, opened.layer);
|
|
1836
|
+
return { role, gitPath: artifactGitPath(name), layer: opened.layer, fd: opened.fd };
|
|
1549
1837
|
}
|
|
1550
1838
|
|
|
1551
1839
|
function formatPreservedArtifacts(artifacts) {
|
|
@@ -1600,10 +1888,10 @@ export function writePlanSafely({
|
|
|
1600
1888
|
const finalName = components.pop();
|
|
1601
1889
|
let parentHandle;
|
|
1602
1890
|
let vaultHandle;
|
|
1603
|
-
let
|
|
1891
|
+
let tempRef;
|
|
1604
1892
|
let tempName;
|
|
1605
1893
|
let tempFd;
|
|
1606
|
-
let
|
|
1894
|
+
let finalRef;
|
|
1607
1895
|
let expectedTemp;
|
|
1608
1896
|
let originalDestination;
|
|
1609
1897
|
let priorBackup;
|
|
@@ -1611,7 +1899,6 @@ export function writePlanSafely({
|
|
|
1611
1899
|
try {
|
|
1612
1900
|
parentHandle = openPlanParent(repo, components);
|
|
1613
1901
|
vaultHandle = openBackupVault(repo);
|
|
1614
|
-
resolveAtomicMover();
|
|
1615
1902
|
const parentDevice = fs.fstatSync(parentHandle.fd, { bigint: true }).dev;
|
|
1616
1903
|
const vaultDevice = fs.fstatSync(vaultHandle.fd, { bigint: true }).dev;
|
|
1617
1904
|
if (parentDevice !== vaultDevice) {
|
|
@@ -1622,19 +1909,11 @@ export function writePlanSafely({
|
|
|
1622
1909
|
testHooks?.afterParentOpen?.({ fd: parentHandle.fd, path: parentHandle.expectedPath });
|
|
1623
1910
|
validatePlanParent(parentHandle);
|
|
1624
1911
|
validatePlanParent(vaultHandle);
|
|
1625
|
-
|
|
1626
|
-
originalDestination = openExistingPlanDestination(
|
|
1912
|
+
finalRef = anchoredChild(parentHandle, finalName);
|
|
1913
|
+
originalDestination = openExistingPlanDestination(finalRef, shouldReplace);
|
|
1627
1914
|
tempName = `.gitnexus-plan-${process.pid}-${randomBytes(16).toString('hex')}.tmp`;
|
|
1628
|
-
|
|
1629
|
-
tempFd =
|
|
1630
|
-
tempPath,
|
|
1631
|
-
fs.constants.O_RDWR |
|
|
1632
|
-
fs.constants.O_CREAT |
|
|
1633
|
-
fs.constants.O_EXCL |
|
|
1634
|
-
fs.constants.O_NOFOLLOW |
|
|
1635
|
-
(fs.constants.O_CLOEXEC ?? 0),
|
|
1636
|
-
0o600,
|
|
1637
|
-
);
|
|
1915
|
+
tempRef = anchoredChild(parentHandle, tempName);
|
|
1916
|
+
tempFd = createChild(tempRef, VERIFIED_CREATE_FLAGS, 0o600);
|
|
1638
1917
|
writeAll(tempFd, contents);
|
|
1639
1918
|
fs.fchmodSync(tempFd, 0o644);
|
|
1640
1919
|
fs.fsyncSync(tempFd);
|
|
@@ -1646,12 +1925,12 @@ export function writePlanSafely({
|
|
|
1646
1925
|
testHooks?.beforeRename?.({
|
|
1647
1926
|
fd: parentHandle.fd,
|
|
1648
1927
|
path: parentHandle.expectedPath,
|
|
1649
|
-
tempPath,
|
|
1928
|
+
tempPath: tempRef.path,
|
|
1650
1929
|
});
|
|
1651
1930
|
validatePlanParent(parentHandle);
|
|
1652
1931
|
validatePlanParent(vaultHandle);
|
|
1653
1932
|
validateOpenPlanDestination(originalDestination);
|
|
1654
|
-
const tempPathStat =
|
|
1933
|
+
const tempPathStat = lstatChild(tempRef);
|
|
1655
1934
|
const currentTemp = hashOpenFile(tempFd, 'generated-plan temporary file');
|
|
1656
1935
|
if (
|
|
1657
1936
|
tempPathStat.isSymbolicLink() ||
|
|
@@ -1664,7 +1943,7 @@ export function writePlanSafely({
|
|
|
1664
1943
|
}
|
|
1665
1944
|
|
|
1666
1945
|
if (shouldReplace) {
|
|
1667
|
-
testHooks?.beforeBackupMove?.({ fd: parentHandle.fd, finalPath });
|
|
1946
|
+
testHooks?.beforeBackupMove?.({ fd: parentHandle.fd, finalPath: finalRef.path });
|
|
1668
1947
|
const originalLayer = hashOpenFile(originalDestination.fd, 'prior generated plan');
|
|
1669
1948
|
if (originalLayer.digest !== expectedDigest) {
|
|
1670
1949
|
throw new Error(
|
|
@@ -1673,7 +1952,7 @@ export function writePlanSafely({
|
|
|
1673
1952
|
}
|
|
1674
1953
|
validatePlanParent(parentHandle);
|
|
1675
1954
|
validateOpenPlanDestination(originalDestination);
|
|
1676
|
-
inspectPlanDestination(
|
|
1955
|
+
inspectPlanDestination(finalRef, {
|
|
1677
1956
|
replace: true,
|
|
1678
1957
|
expectedIdentity: originalDestination.identity,
|
|
1679
1958
|
});
|
|
@@ -1691,20 +1970,20 @@ export function writePlanSafely({
|
|
|
1691
1970
|
);
|
|
1692
1971
|
throw new Error('Destination raced while the prior plan was moved into preservation');
|
|
1693
1972
|
}
|
|
1694
|
-
if (
|
|
1973
|
+
if (lstatAnchoredOptional(finalRef)) {
|
|
1695
1974
|
throw new Error('Destination reappeared after the prior plan was preserved');
|
|
1696
1975
|
}
|
|
1697
1976
|
}
|
|
1698
1977
|
|
|
1699
1978
|
testHooks?.beforePublication?.({
|
|
1700
1979
|
fd: parentHandle.fd,
|
|
1701
|
-
finalPath,
|
|
1702
|
-
tempPath,
|
|
1980
|
+
finalPath: finalRef.path,
|
|
1981
|
+
tempPath: tempRef.path,
|
|
1703
1982
|
replace: shouldReplace,
|
|
1704
1983
|
});
|
|
1705
1984
|
validatePlanParent(parentHandle);
|
|
1706
1985
|
validatePlanParent(vaultHandle);
|
|
1707
|
-
const finalTempPathStat =
|
|
1986
|
+
const finalTempPathStat = lstatChild(tempRef);
|
|
1708
1987
|
const finalTemp = hashOpenFile(tempFd, 'generated-plan temporary file');
|
|
1709
1988
|
if (
|
|
1710
1989
|
finalTempPathStat.isSymbolicLink() ||
|
|
@@ -1715,19 +1994,25 @@ export function writePlanSafely({
|
|
|
1715
1994
|
) {
|
|
1716
1995
|
throw new Error('Generated-plan temporary path or content changed at publication');
|
|
1717
1996
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
);
|
|
1722
|
-
if (lstatOptional(tempPath) || !lstatOptional(finalPath)) {
|
|
1997
|
+
// link() reports the race itself; re-deriving that verdict from a later pair
|
|
1998
|
+
// of stats would be both slower and weaker.
|
|
1999
|
+
if (!publishNoReplace(tempRef, finalRef)) {
|
|
1723
2000
|
throw new Error('Generated-plan publication was refused because the destination raced');
|
|
1724
2001
|
}
|
|
2002
|
+
// link() creates a directory entry, so it needs the parent fsync that rename
|
|
2003
|
+
// needed: the file's own bytes were fsynced through tempFd before this point,
|
|
2004
|
+
// and this makes the name that now reaches them durable too. Skipping it is
|
|
2005
|
+
// the step write-file-atomic omits and maildir, git and atomicwrites all
|
|
2006
|
+
// mandate.
|
|
2007
|
+
//
|
|
2008
|
+
// Honest limitation: on macOS fsync is not a write barrier — the durable
|
|
2009
|
+
// primitive there is fcntl(F_FULLFSYNC), which Node does not expose. A
|
|
2010
|
+
// macOS plan write is therefore as durable as fsync makes it and no more.
|
|
1725
2011
|
fs.fsyncSync(parentHandle.fd);
|
|
1726
|
-
testHooks?.afterPublication?.({ fd: parentHandle.fd, finalPath });
|
|
1727
|
-
testHooks?.afterRename?.({ fd: parentHandle.fd, finalPath });
|
|
2012
|
+
testHooks?.afterPublication?.({ fd: parentHandle.fd, finalPath: finalRef.path });
|
|
1728
2013
|
validatePlanParent(parentHandle);
|
|
1729
2014
|
validatePlanParent(vaultHandle);
|
|
1730
|
-
validateCommittedPlan(
|
|
2015
|
+
validateCommittedPlan(finalRef, tempFd, expectedTemp, testHooks);
|
|
1731
2016
|
const receipt = { generated_plan_path: generatedPlan, bytes_written: contents.length };
|
|
1732
2017
|
if (priorBackup) receipt.prior_plan_backup_git_path = priorBackup.gitPath;
|
|
1733
2018
|
return receipt;
|
|
@@ -1848,6 +2133,11 @@ export function snapshotEvidence({
|
|
|
1848
2133
|
const headGuards = captureHeadGuards(repo);
|
|
1849
2134
|
const dirty = initialDirty.records;
|
|
1850
2135
|
const mutationGuards = [];
|
|
2136
|
+
// Per-snapshot walk state: `absenceCache` owns every descriptor an absence
|
|
2137
|
+
// anchor holds, deduplicated by repo-relative prefix and closed exactly once
|
|
2138
|
+
// below; `guardedDirectories` keeps parent guarding to one stat per directory.
|
|
2139
|
+
const absenceCache = new Map();
|
|
2140
|
+
const walkState = { absenceCache, guardedDirectories: new Set() };
|
|
1851
2141
|
|
|
1852
2142
|
try {
|
|
1853
2143
|
testHooks?.afterAnchorCapture?.({ headCommit: head });
|
|
@@ -1862,7 +2152,9 @@ export function snapshotEvidence({
|
|
|
1862
2152
|
testHooks?.afterGitLayerLoad?.({ headCommit: head });
|
|
1863
2153
|
const globalEntries = [...dirty.values()]
|
|
1864
2154
|
.filter((record) => record.path !== generatedPlan)
|
|
1865
|
-
.map((record) =>
|
|
2155
|
+
.map((record) =>
|
|
2156
|
+
materializeRecord(repo, record, layers, mutationGuards, testHooks, walkState),
|
|
2157
|
+
);
|
|
1866
2158
|
const citedEntries = [...normalizedCitations].sort(compareUtf8).map((repoPath) => {
|
|
1867
2159
|
const status = dirty.get(repoPath) ?? {
|
|
1868
2160
|
path: repoPath,
|
|
@@ -1871,7 +2163,7 @@ export function snapshotEvidence({
|
|
|
1871
2163
|
rename_to: null,
|
|
1872
2164
|
has_untracked: false,
|
|
1873
2165
|
};
|
|
1874
|
-
const entry = materializeRecord(repo, status, layers, mutationGuards, testHooks);
|
|
2166
|
+
const entry = materializeRecord(repo, status, layers, mutationGuards, testHooks, walkState);
|
|
1875
2167
|
const present = Object.values(entry.object_kind).some((kind) => kind !== ABSENT);
|
|
1876
2168
|
if (!present) entry.state = ABSENT;
|
|
1877
2169
|
else if (entry.state === 'clean' && entry.object_kind.untracked !== ABSENT) {
|
|
@@ -1906,21 +2198,13 @@ export function snapshotEvidence({
|
|
|
1906
2198
|
throw new Error(`${guard.absolute} changed before evidence materialization completed`);
|
|
1907
2199
|
}
|
|
1908
2200
|
} else if (guard.type === 'absence') {
|
|
2201
|
+
// statIdentity is a strict superset of stableDirectoryIdentity on the
|
|
2202
|
+
// same stat, so comparing both could only ever fire together.
|
|
1909
2203
|
const parent = fs.fstatSync(guard.fd, { bigint: true });
|
|
1910
|
-
if (
|
|
1911
|
-
!parent.isDirectory() ||
|
|
1912
|
-
stableDirectoryIdentity(parent) !== guard.parentIdentity ||
|
|
1913
|
-
statIdentity(parent) !== guard.parentMutationIdentity
|
|
1914
|
-
) {
|
|
2204
|
+
if (!parent.isDirectory() || statIdentity(parent) !== guard.parentMutationIdentity) {
|
|
1915
2205
|
throw new Error(`Absence anchor changed for ${guard.repoPath}`);
|
|
1916
2206
|
}
|
|
1917
|
-
|
|
1918
|
-
fs.lstatSync(descriptorPath(guard.fd, guard.childName), { bigint: true });
|
|
1919
|
-
} catch (error) {
|
|
1920
|
-
if (error?.code === 'ENOENT') continue;
|
|
1921
|
-
throw error;
|
|
1922
|
-
}
|
|
1923
|
-
throw new Error(`${guard.repoPath} appeared before evidence materialization completed`);
|
|
2207
|
+
anchoringBackend().verifyAbsentChild(guard);
|
|
1924
2208
|
}
|
|
1925
2209
|
}
|
|
1926
2210
|
for (const guard of headGuards) verifyControlFile(guard);
|
|
@@ -1955,12 +2239,10 @@ export function snapshotEvidence({
|
|
|
1955
2239
|
cited_path_manifest: citedEntries,
|
|
1956
2240
|
};
|
|
1957
2241
|
} finally {
|
|
1958
|
-
|
|
1959
|
-
for (const
|
|
1960
|
-
if (guard.type !== 'absence' || closed.has(guard.fd)) continue;
|
|
1961
|
-
closed.add(guard.fd);
|
|
2242
|
+
// One entry per distinct anchored directory, so one close per descriptor.
|
|
2243
|
+
for (const handle of absenceCache.values()) {
|
|
1962
2244
|
try {
|
|
1963
|
-
fs.closeSync(
|
|
2245
|
+
fs.closeSync(handle.fd);
|
|
1964
2246
|
} catch {
|
|
1965
2247
|
// Preserve the primary snapshot result/error.
|
|
1966
2248
|
}
|