kibi-cli 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-json-command.d.ts +6 -0
- package/dist/cli-json-command.d.ts.map +1 -1
- package/dist/cli-json-command.js +37 -9
- package/dist/cli-operation-loader.d.ts.map +1 -1
- package/dist/cli-operation-loader.js +7 -0
- package/dist/cli-operation-metadata.d.ts +12 -4
- package/dist/cli-operation-metadata.d.ts.map +1 -1
- package/dist/cli-operation-metadata.js +14 -4
- package/dist/cli-protocol.d.ts +14 -0
- package/dist/cli-protocol.d.ts.map +1 -1
- package/dist/cli-protocol.js +31 -2
- package/dist/cli-register-proof.d.ts.map +1 -1
- package/dist/cli-register-proof.js +13 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +11 -2
- package/dist/commands/discovery-shared.d.ts.map +1 -1
- package/dist/commands/discovery-shared.js +8 -2
- package/dist/commands/engine.d.ts.map +1 -1
- package/dist/commands/engine.js +4 -2
- package/dist/commands/gc.d.ts.map +1 -1
- package/dist/commands/gc.js +7 -0
- package/dist/commands/init-helpers.d.ts.map +1 -1
- package/dist/commands/init-helpers.js +0 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +281 -14
- package/dist/commands/proof-compact.d.ts +9 -0
- package/dist/commands/proof-compact.d.ts.map +1 -0
- package/dist/commands/proof-compact.js +58 -0
- package/dist/commands/prove.d.ts +38 -0
- package/dist/commands/prove.d.ts.map +1 -1
- package/dist/commands/prove.js +222 -47
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +61 -16
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +42 -16
- package/dist/diagnostics.js +1 -1
- package/dist/engine-limits.d.ts +74 -0
- package/dist/engine-limits.d.ts.map +1 -0
- package/dist/engine-limits.js +201 -0
- package/dist/engine.d.ts +10 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +57 -9
- package/dist/extractors/manifest.d.ts +8 -0
- package/dist/extractors/manifest.d.ts.map +1 -1
- package/dist/extractors/manifest.js +34 -5
- package/dist/extractors/markdown.d.ts +8 -0
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +57 -0
- package/dist/extractors/relationships.d.ts.map +1 -1
- package/dist/extractors/relationships.js +2 -0
- package/dist/intent-search.d.ts +4 -1
- package/dist/intent-search.d.ts.map +1 -1
- package/dist/intent-search.js +186 -33
- package/dist/operations/bootstrap/candidate-helpers.d.ts.map +1 -1
- package/dist/operations/bootstrap/candidate-helpers.js +8 -1
- package/dist/operations/bootstrap/candidates.d.ts.map +1 -1
- package/dist/operations/bootstrap/candidates.js +54 -38
- package/dist/operations/bootstrap/generate.d.ts +2 -0
- package/dist/operations/bootstrap/generate.d.ts.map +1 -1
- package/dist/operations/bootstrap/generate.js +87 -33
- package/dist/operations/bootstrap/intent-claims.d.ts.map +1 -1
- package/dist/operations/bootstrap/intent-claims.js +12 -4
- package/dist/operations/bootstrap/markdown-candidates.d.ts +2 -0
- package/dist/operations/bootstrap/markdown-candidates.d.ts.map +1 -1
- package/dist/operations/bootstrap/markdown-candidates.js +32 -10
- package/dist/operations/bootstrap/presentation.d.ts.map +1 -1
- package/dist/operations/bootstrap/presentation.js +6 -2
- package/dist/operations/bootstrap/requirement-claims.d.ts +1 -0
- package/dist/operations/bootstrap/requirement-claims.d.ts.map +1 -1
- package/dist/operations/bootstrap/requirement-claims.js +13 -1
- package/dist/operations/bootstrap/validation.d.ts +4 -0
- package/dist/operations/bootstrap/validation.d.ts.map +1 -0
- package/dist/operations/bootstrap/validation.js +20 -0
- package/dist/operations/migration/inventory-rederive.d.ts +81 -0
- package/dist/operations/migration/inventory-rederive.d.ts.map +1 -0
- package/dist/operations/migration/inventory-rederive.js +484 -0
- package/dist/operations/migration/kb-sources.d.ts +53 -0
- package/dist/operations/migration/kb-sources.d.ts.map +1 -0
- package/dist/operations/migration/kb-sources.js +233 -0
- package/dist/operations/migration/origin-backfill.d.ts +39 -0
- package/dist/operations/migration/origin-backfill.d.ts.map +1 -0
- package/dist/operations/migration/origin-backfill.js +164 -0
- package/dist/operations/migration/polarity-values.d.ts +10 -0
- package/dist/operations/migration/polarity-values.d.ts.map +1 -0
- package/dist/operations/migration/polarity-values.js +60 -0
- package/dist/operations/migration/schema6.d.ts +59 -0
- package/dist/operations/migration/schema6.d.ts.map +1 -0
- package/dist/operations/migration/schema6.js +379 -0
- package/dist/operations/migration/source-paths.d.ts +131 -0
- package/dist/operations/migration/source-paths.d.ts.map +1 -0
- package/dist/operations/migration/source-paths.js +308 -0
- package/dist/operations/migration/superseded-closure.d.ts +54 -0
- package/dist/operations/migration/superseded-closure.d.ts.map +1 -0
- package/dist/operations/migration/superseded-closure.js +187 -0
- package/dist/operations/modeling/logic-modeling.d.ts.map +1 -1
- package/dist/operations/modeling/logic-modeling.js +3 -5
- package/dist/operations/modeling/model-requirement.d.ts.map +1 -1
- package/dist/operations/modeling/model-requirement.js +225 -67
- package/dist/operations/mutation/contradictions.d.ts +16 -0
- package/dist/operations/mutation/contradictions.d.ts.map +1 -1
- package/dist/operations/mutation/contradictions.js +32 -0
- package/dist/operations/mutation/delete.d.ts.map +1 -1
- package/dist/operations/mutation/delete.js +15 -7
- package/dist/operations/mutation/origin.d.ts +21 -0
- package/dist/operations/mutation/origin.d.ts.map +1 -0
- package/dist/operations/mutation/origin.js +78 -0
- package/dist/operations/mutation/plan-apply-journal.d.ts +182 -0
- package/dist/operations/mutation/plan-apply-journal.d.ts.map +1 -0
- package/dist/operations/mutation/plan-apply-journal.js +613 -0
- package/dist/operations/mutation/predicate-vocabulary-guard.d.ts +3 -1
- package/dist/operations/mutation/predicate-vocabulary-guard.d.ts.map +1 -1
- package/dist/operations/mutation/predicate-vocabulary-guard.js +13 -2
- package/dist/operations/mutation/relationships.d.ts +5 -5
- package/dist/operations/mutation/relationships.d.ts.map +1 -1
- package/dist/operations/mutation/relationships.js +31 -11
- package/dist/operations/mutation/serialization.d.ts.map +1 -1
- package/dist/operations/mutation/serialization.js +3 -1
- package/dist/operations/mutation/source-authoring.d.ts +6 -0
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +7 -1
- package/dist/operations/mutation/types.d.ts +14 -0
- package/dist/operations/mutation/types.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.d.ts +30 -3
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +105 -35
- package/dist/operations/mutation/validate-upsert.d.ts +6 -0
- package/dist/operations/mutation/validate-upsert.d.ts.map +1 -1
- package/dist/operations/mutation/validate-upsert.js +34 -30
- package/dist/operations/planning/apply-plan.d.ts.map +1 -1
- package/dist/operations/planning/apply-plan.js +751 -144
- package/dist/operations/planning/compile-intent.d.ts +34 -1
- package/dist/operations/planning/compile-intent.d.ts.map +1 -1
- package/dist/operations/planning/compile-intent.js +314 -98
- package/dist/operations/proof/code-scope.d.ts +28 -0
- package/dist/operations/proof/code-scope.d.ts.map +1 -1
- package/dist/operations/proof/code-scope.js +72 -0
- package/dist/operations/proof/compact-receipts.d.ts +49 -0
- package/dist/operations/proof/compact-receipts.d.ts.map +1 -0
- package/dist/operations/proof/compact-receipts.js +160 -0
- package/dist/operations/proof/ingest-proof.d.ts +2 -0
- package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
- package/dist/operations/proof/ingest-proof.js +44 -23
- package/dist/operations/proof/prune-receipts.d.ts.map +1 -1
- package/dist/operations/proof/prune-receipts.js +5 -9
- package/dist/operations/proof/receipt-compaction.d.ts +48 -0
- package/dist/operations/proof/receipt-compaction.d.ts.map +1 -0
- package/dist/operations/proof/receipt-compaction.js +121 -0
- package/dist/operations/semantic-advisor/analysis-receipt.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analysis-receipt.js +12 -1
- package/dist/operations/semantic-advisor/analyze-prose.d.ts +2 -1
- package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analyze-prose.js +51 -5
- package/dist/operations/semantic-advisor/clauses.d.ts +1 -0
- package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/clauses.js +11 -1
- package/dist/operations/semantic-advisor/conditional-rules.d.ts +33 -0
- package/dist/operations/semantic-advisor/conditional-rules.d.ts.map +1 -0
- package/dist/operations/semantic-advisor/conditional-rules.js +262 -0
- package/dist/operations/semantic-advisor/ingestion-boundary.d.ts +3 -1
- package/dist/operations/semantic-advisor/ingestion-boundary.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/ingestion-boundary.js +14 -6
- package/dist/operations/semantic-advisor/plugin-orchestration.js +1 -1
- package/dist/prolog/codec.d.ts.map +1 -1
- package/dist/prolog/codec.js +2 -1
- package/dist/prolog.d.ts +9 -0
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +56 -10
- package/dist/proof/command-test-report.d.ts +74 -0
- package/dist/proof/command-test-report.d.ts.map +1 -0
- package/dist/proof/command-test-report.js +206 -0
- package/dist/public/branch-resolver.d.ts +1 -0
- package/dist/public/branch-resolver.d.ts.map +1 -1
- package/dist/public/branch-resolver.js +1 -0
- package/dist/public/entity-origin.d.ts +69 -0
- package/dist/public/entity-origin.d.ts.map +1 -0
- package/dist/public/entity-origin.js +142 -0
- package/dist/public/impact/full-kb-quality.d.ts.map +1 -1
- package/dist/public/impact/full-kb-quality.js +1 -0
- package/dist/public/impact/requirement-quality.js +1 -1
- package/dist/public/operations/catalog.d.ts +130 -19
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/catalog.js +3 -0
- package/dist/public/operations/check-executor.d.ts.map +1 -1
- package/dist/public/operations/check-executor.js +8 -0
- package/dist/public/operations/contracts.d.ts +0 -5
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +63 -14
- package/dist/public/operations/discovery-entities.d.ts +15 -0
- package/dist/public/operations/discovery-entities.d.ts.map +1 -1
- package/dist/public/operations/discovery-entities.js +59 -0
- package/dist/public/operations/discovery-executors.d.ts +6 -1
- package/dist/public/operations/discovery-executors.d.ts.map +1 -1
- package/dist/public/operations/discovery-executors.js +65 -6
- package/dist/public/operations/index.d.ts +1 -0
- package/dist/public/operations/index.d.ts.map +1 -1
- package/dist/public/operations/index.js +1 -0
- package/dist/public/operations/migration-plan.d.ts +5 -0
- package/dist/public/operations/migration-plan.d.ts.map +1 -1
- package/dist/public/operations/migration-plan.js +26 -1
- package/dist/public/operations/node-ports.d.ts.map +1 -1
- package/dist/public/operations/node-ports.js +24 -3
- package/dist/public/operations/origin-review.d.ts +51 -0
- package/dist/public/operations/origin-review.d.ts.map +1 -0
- package/dist/public/operations/origin-review.js +307 -0
- package/dist/public/operations/predicate-schema-conformance.js +1 -1
- package/dist/public/operations/repair-plan.d.ts.map +1 -1
- package/dist/public/operations/repair-plan.js +4 -0
- package/dist/public/operations/result-envelope.d.ts.map +1 -1
- package/dist/public/operations/result-envelope.js +17 -5
- package/dist/public/operations/runtime-types.d.ts +9 -0
- package/dist/public/operations/runtime-types.d.ts.map +1 -1
- package/dist/public/operations/schema6-check-actions.d.ts +127 -0
- package/dist/public/operations/schema6-check-actions.d.ts.map +1 -0
- package/dist/public/operations/schema6-check-actions.js +575 -0
- package/dist/public/operations/source-path-dangling.d.ts +17 -0
- package/dist/public/operations/source-path-dangling.d.ts.map +1 -0
- package/dist/public/operations/source-path-dangling.js +66 -0
- package/dist/public/operations/specs/check-rules.generated.d.ts +1 -1
- package/dist/public/operations/specs/check-rules.generated.d.ts.map +1 -1
- package/dist/public/operations/specs/check-rules.generated.js +15 -1
- package/dist/public/operations/specs/check.d.ts +2 -2
- package/dist/public/operations/specs/check.js +1 -1
- package/dist/public/operations/specs/composite.d.ts +351 -0
- package/dist/public/operations/specs/composite.d.ts.map +1 -0
- package/dist/public/operations/specs/composite.js +96 -0
- package/dist/public/operations/specs/discovery.d.ts +8 -3
- package/dist/public/operations/specs/discovery.d.ts.map +1 -1
- package/dist/public/operations/specs/discovery.js +8 -3
- package/dist/public/operations/specs/modeling.d.ts +2 -2
- package/dist/public/operations/specs/modeling.js +2 -2
- package/dist/public/operations/specs/mutation-schemas.d.ts +73 -8
- package/dist/public/operations/specs/mutation-schemas.d.ts.map +1 -1
- package/dist/public/operations/specs/mutation-schemas.js +43 -7
- package/dist/public/operations/specs/mutation.d.ts +1 -1
- package/dist/public/operations/specs/mutation.js +1 -1
- package/dist/public/operations/specs/planning.d.ts +2 -2
- package/dist/public/operations/specs/planning.js +4 -4
- package/dist/public/operations/specs/reporting.d.ts.map +1 -1
- package/dist/public/operations/specs/reporting.js +13 -26
- package/dist/public/operations/specs/upsert.d.ts +79 -9
- package/dist/public/operations/specs/upsert.d.ts.map +1 -1
- package/dist/public/operations/specs/upsert.js +6 -1
- package/dist/public/operations/types.d.ts +1 -1
- package/dist/public/operations/types.d.ts.map +1 -1
- package/dist/public/proof-receipt.d.ts +1 -0
- package/dist/public/proof-receipt.d.ts.map +1 -1
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +52 -0
- package/dist/public/schemas/relationship.d.ts.map +1 -1
- package/dist/public/schemas/relationship.js +2 -0
- package/dist/public/skills/kibi-bootstrap/SKILL.md +6 -1
- package/dist/public/skills/kibi-bootstrap/resources/bootstrap.md +6 -0
- package/dist/public/skills/kibi-usage/SKILL.md +68 -18
- package/dist/public/skills/kibi-usage/resources/fact-lanes.md +5 -5
- package/dist/public/skills/kibi-usage/resources/kb-improvement.md +1 -1
- package/dist/public/skills/kibi-usage/resources/logic-ir.md +2 -2
- package/dist/public/skills/kibi-usage/resources/operation-access.md +25 -1
- package/dist/public/skills/kibi-usage/resources/ui-requirements.md +4 -4
- package/dist/public/skills/kibi-usage/resources/workflows.md +42 -25
- package/dist/public/telemetry-acceptance.d.ts +58 -2
- package/dist/public/telemetry-acceptance.d.ts.map +1 -1
- package/dist/public/telemetry-acceptance.js +136 -10
- package/dist/public/telemetry-remediation.d.ts.map +1 -1
- package/dist/public/telemetry-remediation.js +26 -10
- package/dist/relationships/shards.d.ts +13 -0
- package/dist/relationships/shards.d.ts.map +1 -1
- package/dist/relationships/shards.js +54 -28
- package/dist/runtime/cli-runtime.d.ts.map +1 -1
- package/dist/runtime/cli-runtime.js +44 -3
- package/dist/runtime/detached-snapshot.d.ts +38 -0
- package/dist/runtime/detached-snapshot.d.ts.map +1 -0
- package/dist/runtime/detached-snapshot.js +92 -0
- package/dist/schemas/entity.schema.json +122 -0
- package/dist/search-answer.d.ts +157 -0
- package/dist/search-answer.d.ts.map +1 -0
- package/dist/search-answer.js +846 -0
- package/dist/traceability/impact-knowledge.d.ts.map +1 -1
- package/dist/traceability/impact-knowledge.js +7 -0
- package/dist/traceability/symbol-extract.js +3 -3
- package/dist/traceability/temp-kb.d.ts.map +1 -1
- package/dist/traceability/temp-kb.js +12 -0
- package/dist/types/entities.d.ts +13 -0
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/types/relationships.d.ts +1 -1
- package/dist/types/relationships.d.ts.map +1 -1
- package/dist/utils/branch-resolver.d.ts +37 -1
- package/dist/utils/branch-resolver.d.ts.map +1 -1
- package/dist/utils/branch-resolver.js +130 -3
- package/dist/utils/repo-relative-path.d.ts +18 -0
- package/dist/utils/repo-relative-path.d.ts.map +1 -0
- package/dist/utils/repo-relative-path.js +58 -0
- package/dist/utils/rule-registry.generated.d.ts +75 -3
- package/dist/utils/rule-registry.generated.d.ts.map +1 -1
- package/dist/utils/rule-registry.generated.js +89 -3
- package/dist/utils/schema-version.d.ts +1 -1
- package/dist/utils/schema-version.d.ts.map +1 -1
- package/dist/utils/schema-version.js +4 -3
- package/dist/utils/strict-modeling.d.ts +1 -1
- package/dist/utils/strict-modeling.d.ts.map +1 -1
- package/dist/utils/strict-modeling.js +23 -3
- package/package.json +3 -3
- package/schema/entities.pl +22 -0
- package/schema/relationships.pl +14 -0
- package/schema/validation.pl +45 -1
- package/src/public/branch-resolver.ts +1 -0
- package/src/public/entity-origin.ts +175 -0
- package/src/public/impact/full-kb-quality.ts +1 -0
- package/src/public/impact/requirement-quality.ts +1 -1
- package/src/public/operations/catalog.ts +3 -0
- package/src/public/operations/check-executor.ts +15 -0
- package/src/public/operations/contracts.ts +75 -15
- package/src/public/operations/discovery-entities.ts +74 -0
- package/src/public/operations/discovery-executors.ts +88 -10
- package/src/public/operations/index.ts +1 -0
- package/src/public/operations/migration-plan.ts +30 -1
- package/src/public/operations/node-ports.ts +24 -3
- package/src/public/operations/origin-review.ts +375 -0
- package/src/public/operations/predicate-schema-conformance.ts +1 -1
- package/src/public/operations/repair-plan.ts +4 -0
- package/src/public/operations/result-envelope.ts +18 -6
- package/src/public/operations/runtime-types.ts +9 -0
- package/src/public/operations/schema6-check-actions.ts +699 -0
- package/src/public/operations/source-path-dangling.ts +81 -0
- package/src/public/operations/specs/check-rules.generated.ts +15 -1
- package/src/public/operations/specs/check.ts +1 -1
- package/src/public/operations/specs/composite.ts +141 -0
- package/src/public/operations/specs/discovery.ts +9 -3
- package/src/public/operations/specs/modeling.ts +2 -2
- package/src/public/operations/specs/mutation-schemas.ts +49 -7
- package/src/public/operations/specs/mutation.ts +1 -1
- package/src/public/operations/specs/planning.ts +4 -4
- package/src/public/operations/specs/reporting.ts +13 -38
- package/src/public/operations/specs/upsert.ts +7 -1
- package/src/public/operations/types.ts +2 -0
- package/src/public/proof-receipt.ts +1 -0
- package/src/public/schemas/entity.ts +57 -0
- package/src/public/schemas/relationship.ts +2 -0
- package/src/public/skills/kibi-bootstrap/SKILL.md +6 -1
- package/src/public/skills/kibi-bootstrap/resources/bootstrap.md +6 -0
- package/src/public/skills/kibi-usage/SKILL.md +68 -18
- package/src/public/skills/kibi-usage/resources/fact-lanes.md +5 -5
- package/src/public/skills/kibi-usage/resources/kb-improvement.md +1 -1
- package/src/public/skills/kibi-usage/resources/logic-ir.md +2 -2
- package/src/public/skills/kibi-usage/resources/operation-access.md +25 -1
- package/src/public/skills/kibi-usage/resources/ui-requirements.md +4 -4
- package/src/public/skills/kibi-usage/resources/workflows.md +42 -25
- package/src/public/telemetry-acceptance-types.d.ts +4 -1
- package/src/public/telemetry-acceptance.ts +204 -10
- package/src/public/telemetry-remediation.ts +39 -10
- package/src/schemas/entity.schema.json +122 -0
- package/src/schemas/relationship.schema.json +2 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeSync, } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { InputError, OperationError } from "../../cli-errors.js";
|
|
5
|
+
import { escapeAtom } from "../../prolog/codec.js";
|
|
6
|
+
import { resolveBranchAttachment } from "../../utils/branch-resolver.js";
|
|
7
|
+
import { branchStoreKey, branchStoreManifestPath, } from "../../utils/branch-store-locator.js";
|
|
8
|
+
import { RELATIONSHIP_TYPES } from "./relationships.js";
|
|
9
|
+
import { retirePendingSourceReceipt, writePendingSourceReceipt, } from "./source-authoring.js";
|
|
10
|
+
/**
|
|
11
|
+
* Write-ahead journal for atomic kb_apply_plan application.
|
|
12
|
+
*
|
|
13
|
+
* A compile plan changes two resources: workspace files (the plan's
|
|
14
|
+
* sourceWrites and the relationship shards its steps append) and the compiled
|
|
15
|
+
* branch store. Before the first write, the journal records every intended
|
|
16
|
+
* file change with exact before/after bytes and hashes, every store upsert,
|
|
17
|
+
* and a fingerprint of the store entities the plan touches. Files are then
|
|
18
|
+
* published with temp-file + rename (fsync where the port supports it), and
|
|
19
|
+
* the store mutations commit in one Prolog transaction. The store commit is
|
|
20
|
+
* the only commit point: any failure before it restores every file from the
|
|
21
|
+
* journal, and an interrupted application is completed or rolled back from
|
|
22
|
+
* the journal by the next mutating call.
|
|
23
|
+
*/
|
|
24
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2, REQ-core-journaled-engine-persistence
|
|
25
|
+
export const PLAN_APPLY_JOURNAL_VERSION = "kibi.plan-apply-journal.v1";
|
|
26
|
+
const JOURNAL_DIRECTORY = "plan-apply";
|
|
27
|
+
const JOURNAL_ID_PATTERN = /^plan-apply-[a-f0-9]{16}$/;
|
|
28
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2
|
|
29
|
+
export function planApplyJournalId(planHash) {
|
|
30
|
+
return `plan-apply-${planHash.slice(0, 16).toLowerCase()}`;
|
|
31
|
+
}
|
|
32
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2
|
|
33
|
+
export function isPlanApplyJournalId(value) {
|
|
34
|
+
return JOURNAL_ID_PATTERN.test(value);
|
|
35
|
+
}
|
|
36
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2
|
|
37
|
+
export function contentHash(value) {
|
|
38
|
+
return createHash("sha256").update(value).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
function errorText(error) {
|
|
41
|
+
return error instanceof Error ? error.message : String(error);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Journals live in the branch store directory, next to the compiled store
|
|
45
|
+
* they guard. Before the store has been created (no identity manifest yet)
|
|
46
|
+
* they live in the branch-keyed `.kb/recovery/plan-apply/<key>` directory, so
|
|
47
|
+
* the store's adoption check never finds unexpected entries.
|
|
48
|
+
*/
|
|
49
|
+
function journalLocation(context) {
|
|
50
|
+
const attachment = context.branchAttachment ?? resolveBranchAttachment(context.workspaceRoot);
|
|
51
|
+
if ("error" in attachment)
|
|
52
|
+
return null;
|
|
53
|
+
const storeDirectory = path.join(attachment.storePath, JOURNAL_DIRECTORY);
|
|
54
|
+
const fallback = path.join(path.resolve(context.workspaceRoot), ".kb", "recovery", JOURNAL_DIRECTORY, branchStoreKey(attachment.kbBranch));
|
|
55
|
+
let storeReady = false;
|
|
56
|
+
try {
|
|
57
|
+
storeReady =
|
|
58
|
+
statSync(attachment.storePath).isDirectory() &&
|
|
59
|
+
existsSync(branchStoreManifestPath(attachment.storePath));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
storeReady = false;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
branch: attachment.kbBranch,
|
|
66
|
+
primary: storeReady ? storeDirectory : fallback,
|
|
67
|
+
all: [...new Set([storeDirectory, fallback])],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function fsyncDirectory(directory) {
|
|
71
|
+
if (process.platform === "win32")
|
|
72
|
+
return;
|
|
73
|
+
try {
|
|
74
|
+
const descriptor = openSync(directory, "r");
|
|
75
|
+
try {
|
|
76
|
+
fsyncSync(descriptor);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
closeSync(descriptor);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Directory fsync is best effort; the journal file itself is synced.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Publish the journal with temp-file + fsync + rename + directory fsync. */
|
|
87
|
+
function writeJournal(journalPath, journal) {
|
|
88
|
+
const directory = path.dirname(journalPath);
|
|
89
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
90
|
+
const temporary = `${journalPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
91
|
+
try {
|
|
92
|
+
const descriptor = openSync(temporary, "w", 0o600);
|
|
93
|
+
try {
|
|
94
|
+
writeSync(descriptor, `${JSON.stringify(journal, null, 2)}\n`);
|
|
95
|
+
fsyncSync(descriptor);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
closeSync(descriptor);
|
|
99
|
+
}
|
|
100
|
+
renameSync(temporary, journalPath);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
try {
|
|
104
|
+
unlinkSync(temporary);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// The temporary file may never have been created.
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
fsyncDirectory(directory);
|
|
112
|
+
}
|
|
113
|
+
function isHash(value) {
|
|
114
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
115
|
+
}
|
|
116
|
+
function isFileWrite(value) {
|
|
117
|
+
if (value === null || typeof value !== "object")
|
|
118
|
+
return false;
|
|
119
|
+
const entry = value;
|
|
120
|
+
const before = entry.before;
|
|
121
|
+
const after = entry.after;
|
|
122
|
+
return (typeof entry.path === "string" &&
|
|
123
|
+
entry.path !== "" &&
|
|
124
|
+
!path.isAbsolute(entry.path) &&
|
|
125
|
+
!entry.path.split(/[\\/]/).includes("..") &&
|
|
126
|
+
(entry.origin === "plan" ||
|
|
127
|
+
entry.origin === "entity-document" ||
|
|
128
|
+
entry.origin === "relationship-shard") &&
|
|
129
|
+
(entry.mode === "write" || entry.mode === "delete") &&
|
|
130
|
+
(before === null
|
|
131
|
+
? entry.beforeHash === null
|
|
132
|
+
: typeof before === "string" &&
|
|
133
|
+
entry.beforeHash === contentHash(before)) &&
|
|
134
|
+
(after === null
|
|
135
|
+
? entry.afterHash === null
|
|
136
|
+
: typeof after === "string" && entry.afterHash === contentHash(after)) &&
|
|
137
|
+
(entry.mode === "delete") === (after === null));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Parse a journal and check its internal consistency: every recorded file
|
|
141
|
+
* carries bytes whose hashes match the recorded before/after hashes.
|
|
142
|
+
*/
|
|
143
|
+
function parseJournal(journalId, text) {
|
|
144
|
+
const parsed = JSON.parse(text);
|
|
145
|
+
const terminal = parsed.state === "committed" || parsed.state === "rolled_back";
|
|
146
|
+
if (parsed.version !== PLAN_APPLY_JOURNAL_VERSION ||
|
|
147
|
+
parsed.journalId !== journalId ||
|
|
148
|
+
!isHash(parsed.planHash) ||
|
|
149
|
+
planApplyJournalId(parsed.planHash) !== journalId ||
|
|
150
|
+
typeof parsed.branch !== "string" ||
|
|
151
|
+
![
|
|
152
|
+
"prepared",
|
|
153
|
+
"store_committing",
|
|
154
|
+
"store_committed",
|
|
155
|
+
"committed",
|
|
156
|
+
"rolled_back",
|
|
157
|
+
].includes(String(parsed.state)) ||
|
|
158
|
+
!Array.isArray(parsed.files) ||
|
|
159
|
+
!parsed.files.every(isFileWrite) ||
|
|
160
|
+
parsed.store === undefined ||
|
|
161
|
+
!Array.isArray(parsed.store.entityIds) ||
|
|
162
|
+
!parsed.store.entityIds.every((id) => typeof id === "string") ||
|
|
163
|
+
typeof parsed.store.preCommitFingerprint !== "string" ||
|
|
164
|
+
!Array.isArray(parsed.store.entries) ||
|
|
165
|
+
(!terminal && parsed.store.entries.length === 0) ||
|
|
166
|
+
parsed.summary === undefined) {
|
|
167
|
+
throw new Error("journal fields are missing or inconsistent");
|
|
168
|
+
}
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
function unrecoverableJournal(journalPath, detail) {
|
|
172
|
+
throw new OperationError("PARTIAL_COMMIT_REPAIR_REQUIRED", `Plan application journal ${journalPath} cannot be recovered automatically: ${detail}. Kibi will not apply or recover plans on this branch until it is resolved: restore each listed file to its journaled before or after bytes and retry, or reconcile the workspace and branch store by hand (kibi sync) and remove the journal`, false);
|
|
173
|
+
}
|
|
174
|
+
function readJournalFile(journalPath) {
|
|
175
|
+
const journalId = path.basename(journalPath, ".json");
|
|
176
|
+
let text;
|
|
177
|
+
try {
|
|
178
|
+
text = readFileSync(journalPath, "utf8");
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
unrecoverableJournal(journalPath, `unreadable (${errorText(error)})`);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
return parseJournal(journalId, text);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
unrecoverableJournal(journalPath, `invalid (${errorText(error)})`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2, REQ-core-journaled-engine-persistence
|
|
191
|
+
/**
|
|
192
|
+
* The terminal-or-absent journal slot for a plan. Returns the existing
|
|
193
|
+
* journal (if any) so the caller can refuse a plan that already committed.
|
|
194
|
+
*/
|
|
195
|
+
export function openPlanApplyJournal(context, planHash) {
|
|
196
|
+
const location = journalLocation(context);
|
|
197
|
+
if (location === null) {
|
|
198
|
+
throw new Error("Apply plan failed before any write: the KB branch could not be resolved for the plan journal");
|
|
199
|
+
}
|
|
200
|
+
const journalId = planApplyJournalId(planHash);
|
|
201
|
+
let existing = null;
|
|
202
|
+
let journalPath = path.join(location.primary, `${journalId}.json`);
|
|
203
|
+
for (const directory of location.all) {
|
|
204
|
+
const candidate = path.join(directory, `${journalId}.json`);
|
|
205
|
+
if (existsSync(candidate)) {
|
|
206
|
+
existing = readJournalFile(candidate);
|
|
207
|
+
journalPath = candidate;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { journalId, journalPath, branch: location.branch, existing };
|
|
212
|
+
}
|
|
213
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2, REQ-core-journaled-engine-persistence
|
|
214
|
+
/** Durably record a journal state; the handle tracks the latest record. */
|
|
215
|
+
export function recordPlanApplyJournal(handle, update, now = new Date()) {
|
|
216
|
+
const next = {
|
|
217
|
+
...handle.journal,
|
|
218
|
+
...update,
|
|
219
|
+
updatedAt: now.toISOString(),
|
|
220
|
+
};
|
|
221
|
+
const terminal = next.state === "committed" || next.state === "rolled_back";
|
|
222
|
+
// Terminal journals keep only their summary: the bytes and store payload
|
|
223
|
+
// are no longer needed and the record stays small.
|
|
224
|
+
const compacted = terminal
|
|
225
|
+
? { ...next, files: [], store: { ...next.store, entries: [] } }
|
|
226
|
+
: next;
|
|
227
|
+
writeJournal(handle.journalPath, compacted);
|
|
228
|
+
handle.journal = compacted;
|
|
229
|
+
}
|
|
230
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2, REQ-core-journaled-engine-persistence
|
|
231
|
+
export function createPlanApplyJournal(journalPath, journal) {
|
|
232
|
+
writeJournal(journalPath, journal);
|
|
233
|
+
return { journalPath, journal };
|
|
234
|
+
}
|
|
235
|
+
/** Bind pending-source receipts to the bytes a committed plan published. */
|
|
236
|
+
// implements REQ-core-journaled-engine-persistence
|
|
237
|
+
export function reconcilePendingSourceReceipts(workspaceRoot, writes) {
|
|
238
|
+
for (const write of writes) {
|
|
239
|
+
if ((write.mode ?? "write") === "delete") {
|
|
240
|
+
retirePendingSourceReceipt(workspaceRoot, write.path);
|
|
241
|
+
}
|
|
242
|
+
else if (write.afterHash !== null) {
|
|
243
|
+
writePendingSourceReceipt(workspaceRoot, write.path, write.afterHash);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function currentHash(fsPort, absolute) {
|
|
248
|
+
try {
|
|
249
|
+
return contentHash(await fsPort.readFile(absolute));
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// implements REQ-core-journaled-engine-persistence
|
|
256
|
+
/**
|
|
257
|
+
* Replace one workspace file atomically: write a unique staged file in the
|
|
258
|
+
* target directory, fsync it, rename it over the target, then fsync the
|
|
259
|
+
* directory. Ports without rename (test and constrained hosts) write the
|
|
260
|
+
* target directly.
|
|
261
|
+
*/
|
|
262
|
+
export async function publishWorkspaceFile(fsPort, absolute, content) {
|
|
263
|
+
await fsPort.mkdir(path.dirname(absolute));
|
|
264
|
+
if (fsPort.rename) {
|
|
265
|
+
const staged = `${absolute}.kibi-stage-${process.pid}-${randomUUID()}`;
|
|
266
|
+
try {
|
|
267
|
+
await fsPort.writeFile(staged, content);
|
|
268
|
+
if (fsPort.fsync)
|
|
269
|
+
await fsPort.fsync(staged);
|
|
270
|
+
await fsPort.rename(staged, absolute);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (fsPort.unlink)
|
|
274
|
+
await fsPort.unlink(staged).catch(() => undefined);
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
await fsPort.writeFile(absolute, content);
|
|
280
|
+
if (fsPort.fsync)
|
|
281
|
+
await fsPort.fsync(absolute);
|
|
282
|
+
}
|
|
283
|
+
if (fsPort.fsync)
|
|
284
|
+
await fsPort.fsync(path.dirname(absolute)).catch(() => undefined);
|
|
285
|
+
}
|
|
286
|
+
async function removeWorkspaceFile(fsPort, absolute, relative) {
|
|
287
|
+
if (!fsPort.unlink) {
|
|
288
|
+
throw new Error(`Apply plan failed: delete requires filesystem unlink support: ${relative}`);
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
await fsPort.unlink(absolute);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (error.code !== "ENOENT")
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
if (fsPort.fsync)
|
|
298
|
+
await fsPort.fsync(path.dirname(absolute)).catch(() => undefined);
|
|
299
|
+
}
|
|
300
|
+
async function setFile(fsPort, workspaceRoot, relative, content) {
|
|
301
|
+
const absolute = path.resolve(workspaceRoot, relative);
|
|
302
|
+
if (content === null)
|
|
303
|
+
await removeWorkspaceFile(fsPort, absolute, relative);
|
|
304
|
+
else
|
|
305
|
+
await publishWorkspaceFile(fsPort, absolute, content);
|
|
306
|
+
}
|
|
307
|
+
// implements REQ-core-journaled-engine-persistence
|
|
308
|
+
/**
|
|
309
|
+
* Publish every journaled file's after-bytes in journal order. Throws on the
|
|
310
|
+
* first failure; the caller restores the before-bytes from the journal.
|
|
311
|
+
*/
|
|
312
|
+
export async function publishJournaledFiles(fsPort, workspaceRoot, files) {
|
|
313
|
+
for (const file of files) {
|
|
314
|
+
await setFile(fsPort, workspaceRoot, file.path, file.after);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Move every journaled file to one side of the plan. With `verify`, every
|
|
319
|
+
* file must currently hold its journaled before or after bytes, checked for
|
|
320
|
+
* all files before any of them changes; a file another writer changed makes
|
|
321
|
+
* the move refuse without touching anything. Without `verify` (the in-process
|
|
322
|
+
* rollback of a write this call just attempted) files are restored as-is.
|
|
323
|
+
*/
|
|
324
|
+
async function moveJournaledFiles(fsPort, workspaceRoot, journalPath, files, side, verify) {
|
|
325
|
+
const pending = [];
|
|
326
|
+
const conflicts = [];
|
|
327
|
+
for (const file of files) {
|
|
328
|
+
const actual = await currentHash(fsPort, path.resolve(workspaceRoot, file.path));
|
|
329
|
+
const wanted = side === "before" ? file.beforeHash : file.afterHash;
|
|
330
|
+
if (actual === wanted)
|
|
331
|
+
continue;
|
|
332
|
+
const other = side === "before" ? file.afterHash : file.beforeHash;
|
|
333
|
+
if (verify && actual !== other) {
|
|
334
|
+
conflicts.push(file.path);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
pending.push(file);
|
|
338
|
+
}
|
|
339
|
+
if (conflicts.length > 0) {
|
|
340
|
+
unrecoverableJournal(journalPath, `${conflicts.join(", ")} changed outside the journal (neither the journaled before nor after bytes)`);
|
|
341
|
+
}
|
|
342
|
+
const ordered = side === "before" ? [...pending].reverse() : pending;
|
|
343
|
+
for (const file of ordered) {
|
|
344
|
+
await setFile(fsPort, workspaceRoot, file.path, side === "before" ? file.before : file.after);
|
|
345
|
+
}
|
|
346
|
+
return ordered.map((file) => file.path);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* An interrupted publish can leave its unique staged temp file next to the
|
|
350
|
+
* target. Remove those (best effort) once recovery has settled the target.
|
|
351
|
+
*/
|
|
352
|
+
function removeOrphanedStagedFiles(workspaceRoot, files) {
|
|
353
|
+
for (const file of files) {
|
|
354
|
+
const absolute = path.resolve(workspaceRoot, file.path);
|
|
355
|
+
const prefix = `${path.basename(absolute)}.kibi-stage-`;
|
|
356
|
+
let names;
|
|
357
|
+
try {
|
|
358
|
+
names = readdirSync(path.dirname(absolute));
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
for (const name of names) {
|
|
364
|
+
if (!name.startsWith(prefix))
|
|
365
|
+
continue;
|
|
366
|
+
try {
|
|
367
|
+
unlinkSync(path.join(path.dirname(absolute), name));
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
// A leftover staged file is untracked noise, never plan state.
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// implements REQ-core-journaled-engine-persistence
|
|
376
|
+
/** Restore every journaled file's before-bytes (in-process rollback). */
|
|
377
|
+
export async function restoreJournaledFiles(fsPort, workspaceRoot, journalPath, files) {
|
|
378
|
+
return moveJournaledFiles(fsPort, workspaceRoot, journalPath, files, "before", false);
|
|
379
|
+
}
|
|
380
|
+
// implements REQ-core-journaled-engine-persistence
|
|
381
|
+
/**
|
|
382
|
+
* Fingerprint the store state of the entities a plan touches: their sorted
|
|
383
|
+
* properties and outgoing relationships. Comparing the fingerprint taken
|
|
384
|
+
* before the commit with a later one decides whether an interrupted batch
|
|
385
|
+
* commit took effect; the batch is one transaction, so it either changed the
|
|
386
|
+
* plan's entities or changed nothing.
|
|
387
|
+
*/
|
|
388
|
+
export async function storeFingerprint(prolog, entityIds) {
|
|
389
|
+
const ids = entityIds.map((id) => `'${escapeAtom(id)}'`).join(", ");
|
|
390
|
+
const state = `findall(Id-Entities-Relationships, (member(Id, [${ids}]), findall(Type-Sorted, (kb_entity(Id, Type, Props), msort(Props, Sorted)), Entities), findall(Rel-To, (member(Rel, [${RELATIONSHIP_TYPES.join(", ")}]), kb_relationship(Rel, Id, To)), Unsorted), msort(Unsorted, Relationships)), State)`;
|
|
391
|
+
// Hash inside Prolog so large entity properties never cross the
|
|
392
|
+
// transport; the outer findall leaves every intermediate variable unbound,
|
|
393
|
+
// so only the SHA-256 hex digest is reported back.
|
|
394
|
+
const goal = `findall(Hex, (${state}, format(string(StateText), '~q', [State]), sha:sha_hash(StateText, Digest, [algorithm(sha256)]), sha:hash_atom(Digest, Hex)), [FingerprintHash])`;
|
|
395
|
+
// Never answer a fingerprint from a cached read of an earlier state.
|
|
396
|
+
prolog.invalidateCache?.();
|
|
397
|
+
const result = await prolog.query(goal);
|
|
398
|
+
if (!result.success) {
|
|
399
|
+
throw new Error(`store fingerprint query failed: ${result.error ?? "unknown error"}`);
|
|
400
|
+
}
|
|
401
|
+
return contentHash((result.bindings.FingerprintHash ?? "").replace(/^['"]|['"]$/g, ""));
|
|
402
|
+
}
|
|
403
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2, REQ-core-journaled-engine-persistence
|
|
404
|
+
/**
|
|
405
|
+
* Deterministically finish one journal:
|
|
406
|
+
* - prepared: the store was never asked to commit, so every file is restored
|
|
407
|
+
* to its before-bytes and the journal ends rolled_back;
|
|
408
|
+
* - store_committing: the store fingerprint decides. Unchanged since the
|
|
409
|
+
* journal was written means the batch never took effect (roll back);
|
|
410
|
+
* changed means it committed (complete: publish every after-byte);
|
|
411
|
+
* - store_committed: complete;
|
|
412
|
+
* - committed / rolled_back: nothing to do (replay is idempotent).
|
|
413
|
+
* Recovery refuses, changing nothing, when a journaled file holds neither
|
|
414
|
+
* its before nor its after bytes.
|
|
415
|
+
*/
|
|
416
|
+
export async function recoverPlanApplyJournal(context, handle, options) {
|
|
417
|
+
const { journal } = handle;
|
|
418
|
+
const now = options.now ?? (() => new Date());
|
|
419
|
+
if (journal.state === "committed" || journal.state === "rolled_back") {
|
|
420
|
+
return {
|
|
421
|
+
journalId: journal.journalId,
|
|
422
|
+
planHash: journal.planHash,
|
|
423
|
+
action: "none",
|
|
424
|
+
state: journal.state,
|
|
425
|
+
restoredPaths: [],
|
|
426
|
+
detail: `Journal ${journal.journalId} is already ${journal.state === "committed" ? "committed" : "rolled back"}; nothing to recover.`,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const fsPort = context.fs;
|
|
430
|
+
if (!fsPort) {
|
|
431
|
+
throw new Error(`Plan journal ${journal.journalId} recovery requires a filesystem-capable runtime`);
|
|
432
|
+
}
|
|
433
|
+
let committed;
|
|
434
|
+
let reason;
|
|
435
|
+
if (journal.state === "prepared") {
|
|
436
|
+
committed = false;
|
|
437
|
+
reason = "the store commit was never submitted";
|
|
438
|
+
}
|
|
439
|
+
else if (journal.state === "store_committed") {
|
|
440
|
+
committed = true;
|
|
441
|
+
reason = "the store had accepted the batch";
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
const prolog = await options.prolog();
|
|
445
|
+
if (!prolog) {
|
|
446
|
+
throw new OperationError("PLAN_APPLY_RECOVERY_REQUIRED", `Plan journal ${journal.journalId} was interrupted during its store commit; recovering it needs the branch engine, which is unavailable. Retry kb_apply_plan with recoveryJournalId=${journal.journalId} once the engine is available`, false);
|
|
447
|
+
}
|
|
448
|
+
let fingerprint;
|
|
449
|
+
try {
|
|
450
|
+
fingerprint = await storeFingerprint(prolog, journal.store.entityIds);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
throw new OperationError("PLAN_APPLY_RECOVERY_REQUIRED", `Plan journal ${journal.journalId} was interrupted during its store commit and the store could not be inspected (${errorText(error)}). Retry kb_apply_plan with recoveryJournalId=${journal.journalId}`, false);
|
|
454
|
+
}
|
|
455
|
+
committed = fingerprint !== journal.store.preCommitFingerprint;
|
|
456
|
+
reason = committed
|
|
457
|
+
? "the store shows the batch took effect"
|
|
458
|
+
: "the store shows the batch never took effect";
|
|
459
|
+
}
|
|
460
|
+
// Record the decision before touching files, so a second interruption
|
|
461
|
+
// resumes the same direction without inspecting the store again.
|
|
462
|
+
if (committed && journal.state !== "store_committed") {
|
|
463
|
+
recordPlanApplyJournal(handle, { state: "store_committed" }, now());
|
|
464
|
+
}
|
|
465
|
+
const restoredPaths = await moveJournaledFiles(fsPort, context.workspaceRoot, handle.journalPath, journal.files, committed ? "after" : "before", true);
|
|
466
|
+
removeOrphanedStagedFiles(context.workspaceRoot, journal.files);
|
|
467
|
+
if (!committed) {
|
|
468
|
+
const detail = `Rolled back interrupted plan ${journal.planHash.slice(0, 12)} (journal ${journal.journalId}): ${reason}; restored ${restoredPaths.length} of ${journal.files.length} journaled file(s) to their before-bytes.`;
|
|
469
|
+
recordPlanApplyJournal(handle, {
|
|
470
|
+
state: "rolled_back",
|
|
471
|
+
resolution: { action: "rolled_back", by: "recovery", detail },
|
|
472
|
+
}, now());
|
|
473
|
+
return {
|
|
474
|
+
journalId: journal.journalId,
|
|
475
|
+
planHash: journal.planHash,
|
|
476
|
+
action: "rolled_back",
|
|
477
|
+
state: "rolled_back",
|
|
478
|
+
restoredPaths,
|
|
479
|
+
detail,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const detail = `Completed interrupted plan ${journal.planHash.slice(0, 12)} (journal ${journal.journalId}): ${reason}; published ${restoredPaths.length} of ${journal.files.length} journaled file(s) at their after-bytes.`;
|
|
483
|
+
try {
|
|
484
|
+
reconcilePendingSourceReceipts(context.workspaceRoot, journal.files);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
// The store and files are complete; receipts stay pending in the
|
|
488
|
+
// store_committed journal for the next recovery.
|
|
489
|
+
return {
|
|
490
|
+
journalId: journal.journalId,
|
|
491
|
+
planHash: journal.planHash,
|
|
492
|
+
action: "completed",
|
|
493
|
+
state: "committed",
|
|
494
|
+
restoredPaths,
|
|
495
|
+
detail,
|
|
496
|
+
receiptFailure: errorText(error),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
recordPlanApplyJournal(handle, {
|
|
500
|
+
state: "committed",
|
|
501
|
+
resolution: { action: "completed", by: "recovery", detail },
|
|
502
|
+
}, now());
|
|
503
|
+
return {
|
|
504
|
+
journalId: journal.journalId,
|
|
505
|
+
planHash: journal.planHash,
|
|
506
|
+
action: "completed",
|
|
507
|
+
state: "committed",
|
|
508
|
+
restoredPaths,
|
|
509
|
+
detail,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2, REQ-core-journaled-engine-persistence
|
|
513
|
+
/** Open an explicit journal by id for kb_apply_plan recoveryJournalId. */
|
|
514
|
+
export function openPlanApplyJournalById(context, journalId) {
|
|
515
|
+
if (!isPlanApplyJournalId(journalId)) {
|
|
516
|
+
throw new Error("Plan journal ID is invalid");
|
|
517
|
+
}
|
|
518
|
+
const location = journalLocation(context);
|
|
519
|
+
if (location === null) {
|
|
520
|
+
throw new Error("Plan journal recovery failed: the KB branch could not be resolved");
|
|
521
|
+
}
|
|
522
|
+
for (const directory of location.all) {
|
|
523
|
+
const candidate = path.join(directory, `${journalId}.json`);
|
|
524
|
+
if (existsSync(candidate)) {
|
|
525
|
+
return { journalPath: candidate, journal: readJournalFile(candidate) };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
throw new Error(`Plan journal ${journalId} does not exist for branch ${location.branch}`);
|
|
529
|
+
}
|
|
530
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2, REQ-core-journaled-engine-persistence
|
|
531
|
+
/**
|
|
532
|
+
* Detect and finish every incomplete plan journal of the active branch. Runs
|
|
533
|
+
* at the start of each mutating call that holds the workspace mutation lock,
|
|
534
|
+
* before that call reads or writes anything. Journals are finished in path
|
|
535
|
+
* order so the outcome does not depend on directory enumeration order.
|
|
536
|
+
*/
|
|
537
|
+
export async function recoverPendingPlanApplyJournals(context, options) {
|
|
538
|
+
if (!context.fs)
|
|
539
|
+
return [];
|
|
540
|
+
const location = journalLocation(context);
|
|
541
|
+
if (location === null)
|
|
542
|
+
return [];
|
|
543
|
+
const candidates = [];
|
|
544
|
+
for (const directory of location.all) {
|
|
545
|
+
let names;
|
|
546
|
+
try {
|
|
547
|
+
names = readdirSync(directory);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
for (const name of names) {
|
|
553
|
+
if (name.endsWith(".json") && isPlanApplyJournalId(name.slice(0, -5))) {
|
|
554
|
+
candidates.push(path.join(directory, name));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const recoveries = [];
|
|
559
|
+
for (const journalPath of candidates.sort()) {
|
|
560
|
+
const journal = readJournalFile(journalPath);
|
|
561
|
+
if (journal.state === "committed" || journal.state === "rolled_back")
|
|
562
|
+
continue;
|
|
563
|
+
recoveries.push(await recoverPlanApplyJournal(context, { journalPath, journal }, options));
|
|
564
|
+
}
|
|
565
|
+
return recoveries;
|
|
566
|
+
}
|
|
567
|
+
/** One line per journal a recovery actually changed, for result warnings. */
|
|
568
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2
|
|
569
|
+
export function planRecoveryNotes(recoveries) {
|
|
570
|
+
return recoveries
|
|
571
|
+
.filter((recovery) => recovery.action !== "none")
|
|
572
|
+
.map((recovery) => recovery.detail);
|
|
573
|
+
}
|
|
574
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2, REQ-core-journaled-engine-persistence
|
|
575
|
+
/**
|
|
576
|
+
* A mutating call that settled an interrupted plan and then failed on its own
|
|
577
|
+
* work still reports the settlement in its failure text. Typed errors keep
|
|
578
|
+
* their class, code and retryability.
|
|
579
|
+
*/
|
|
580
|
+
export function withPlanRecoveryNotes(error, notes) {
|
|
581
|
+
if (notes.length === 0)
|
|
582
|
+
return error;
|
|
583
|
+
const suffix = ` [settled before this failure: ${notes.join(" ")}]`;
|
|
584
|
+
if (error instanceof OperationError) {
|
|
585
|
+
return new OperationError(error.code, `${error.detail}${suffix}`, error.retryable);
|
|
586
|
+
}
|
|
587
|
+
if (error instanceof InputError) {
|
|
588
|
+
return new InputError(error.code, `${error.detail}${suffix}`);
|
|
589
|
+
}
|
|
590
|
+
if (error instanceof Error) {
|
|
591
|
+
error.message = `${error.message}${suffix}`;
|
|
592
|
+
return error;
|
|
593
|
+
}
|
|
594
|
+
return new Error(`${String(error)}${suffix}`);
|
|
595
|
+
}
|
|
596
|
+
// implements REQ-kibi-change-to-proof-plan-compiler-v2, REQ-core-journaled-engine-persistence
|
|
597
|
+
/**
|
|
598
|
+
* Recover every pending plan journal before another mutation, then refuse
|
|
599
|
+
* that mutation while a recovered plan still has unfinished postcommit work
|
|
600
|
+
* (pending-source receipts): a later write could change the files the
|
|
601
|
+
* journal guards and leave it unrecoverable. `explicitJournalId` is the
|
|
602
|
+
* journal the caller is recovering on purpose, which reports its own
|
|
603
|
+
* unfinished work instead.
|
|
604
|
+
*/
|
|
605
|
+
export async function settlePendingPlanApplyJournals(context, options, explicitJournalId) {
|
|
606
|
+
const recoveries = await recoverPendingPlanApplyJournals(context, options);
|
|
607
|
+
const unfinished = recoveries.find((recovery) => recovery.receiptFailure !== undefined &&
|
|
608
|
+
recovery.journalId !== explicitJournalId);
|
|
609
|
+
if (unfinished !== undefined) {
|
|
610
|
+
throw new OperationError("PLAN_APPLY_RECOVERY_REQUIRED", `${unfinished.detail} Binding its new source files to pending-source receipts failed (${unfinished.receiptFailure}); run kb_apply_plan with recoveryJournalId=${unfinished.journalId} before further writes`, false);
|
|
611
|
+
}
|
|
612
|
+
return recoveries;
|
|
613
|
+
}
|
|
@@ -4,5 +4,7 @@ import type { PrologPort } from "../../public/operations/runtime-types.js";
|
|
|
4
4
|
* project-local schema declares. An alias is rejected too, with the declared
|
|
5
5
|
* constant to use instead, so new facts never need migrating.
|
|
6
6
|
*/
|
|
7
|
-
export declare function assertPredicateArgumentVocabulary(prolog: Pick<PrologPort, "query">, entity: Readonly<Record<string, unknown
|
|
7
|
+
export declare function assertPredicateArgumentVocabulary(prolog: Pick<PrologPort, "query">, entity: Readonly<Record<string, unknown>>, staged?: Readonly<{
|
|
8
|
+
entities: ReadonlyMap<string, Readonly<Record<string, unknown>>>;
|
|
9
|
+
}>): Promise<void>;
|
|
8
10
|
//# sourceMappingURL=predicate-vocabulary-guard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"predicate-vocabulary-guard.d.ts","sourceRoot":"","sources":["../../../src/operations/mutation/predicate-vocabulary-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAY3E;;;;GAIG;AAEH,wBAAsB,iCAAiC,CACrD,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,EACjC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"predicate-vocabulary-guard.d.ts","sourceRoot":"","sources":["../../../src/operations/mutation/predicate-vocabulary-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAY3E;;;;GAIG;AAEH,wBAAsB,iCAAiC,CACrD,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,EACjC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,MAAM,CAAC,EAAE,QAAQ,CAAC;IAChB,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CAClE,CAAC,GACD,OAAO,CAAC,IAAI,CAAC,CA+Df"}
|
|
@@ -28,7 +28,7 @@ function text(value) {
|
|
|
28
28
|
* constant to use instead, so new facts never need migrating.
|
|
29
29
|
*/
|
|
30
30
|
// implements REQ-kibi-predicate-argument-constants
|
|
31
|
-
export async function assertPredicateArgumentVocabulary(prolog, entity) {
|
|
31
|
+
export async function assertPredicateArgumentVocabulary(prolog, entity, staged) {
|
|
32
32
|
if (entity.type !== "fact" || entity.fact_kind !== "predicate")
|
|
33
33
|
return;
|
|
34
34
|
const name = text(entity.predicate_name);
|
|
@@ -42,9 +42,20 @@ export async function assertPredicateArgumentVocabulary(prolog, entity) {
|
|
|
42
42
|
if (!result.success) {
|
|
43
43
|
throw new Error(`Predicate schema lookup failed: ${result.error ?? "unknown error"}`);
|
|
44
44
|
}
|
|
45
|
-
const
|
|
45
|
+
const stored = result.bindings.Results
|
|
46
46
|
? parseListOfLists(result.bindings.Results).map(parseEntityFromList)
|
|
47
47
|
: [];
|
|
48
|
+
// Schemas an earlier plan step writes replace the stored version of the
|
|
49
|
+
// same id and count as declared.
|
|
50
|
+
const plannedSchemas = [...(staged?.entities.values() ?? [])].filter((candidate) => candidate.type === "fact" &&
|
|
51
|
+
candidate.fact_kind === "predicate_schema" &&
|
|
52
|
+
text(candidate.predicate_name) === name &&
|
|
53
|
+
(text(candidate.predicate_namespace) ?? "default") === namespace &&
|
|
54
|
+
Number(candidate.predicate_arity) === args.length);
|
|
55
|
+
const schemas = [
|
|
56
|
+
...stored.filter((schema) => !staged?.entities.has(String(schema.id ?? ""))),
|
|
57
|
+
...plannedSchemas,
|
|
58
|
+
];
|
|
48
59
|
const problems = [];
|
|
49
60
|
for (const schema of schemas) {
|
|
50
61
|
const argumentNames = Array.isArray(schema.argument_names)
|