kibi-cli 2.3.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 +39 -11
- 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 +13 -5
- package/dist/cli-operation-metadata.d.ts.map +1 -1
- package/dist/cli-operation-metadata.js +15 -5
- 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-maintenance.d.ts.map +1 -1
- package/dist/cli-register-maintenance.js +7 -0
- 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/merge-driver.d.ts +31 -0
- package/dist/commands/merge-driver.d.ts.map +1 -0
- package/dist/commands/merge-driver.js +268 -0
- 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 +55 -39
- package/dist/operations/bootstrap/discovery.js +1 -1
- 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 +107 -37
- package/dist/operations/bootstrap/guidance-actions.d.ts.map +1 -1
- package/dist/operations/bootstrap/guidance-actions.js +3 -0
- package/dist/operations/bootstrap/guidance.d.ts.map +1 -1
- package/dist/operations/bootstrap/guidance.js +7 -1
- package/dist/operations/bootstrap/intent-claims.d.ts +17 -0
- package/dist/operations/bootstrap/intent-claims.d.ts.map +1 -0
- package/dist/operations/bootstrap/intent-claims.js +103 -0
- 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 +2 -0
- package/dist/operations/bootstrap/presentation.d.ts.map +1 -1
- package/dist/operations/bootstrap/presentation.js +66 -3
- 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/types.d.ts +28 -0
- package/dist/operations/bootstrap/types.d.ts.map +1 -1
- package/dist/operations/bootstrap/types.js +16 -0
- 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/symbol-refresh.d.ts.map +1 -1
- package/dist/operations/mutation/symbol-refresh.js +4 -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/diagnostic-usage.d.ts +14 -0
- package/dist/public/diagnostic-usage.d.ts.map +1 -1
- package/dist/public/diagnostic-usage.js +55 -1
- 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 +203 -20
- 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/bootstrap.d.ts +73 -1
- package/dist/public/operations/specs/bootstrap.d.ts.map +1 -1
- package/dist/public/operations/specs/bootstrap.js +66 -1
- 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/symbol-repair-plan.d.ts.map +1 -1
- package/dist/public/operations/symbol-repair-plan.js +3 -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 +52 -11
- package/dist/public/skills/kibi-bootstrap/resources/bootstrap.md +12 -1
- package/dist/public/skills/kibi-usage/SKILL.md +82 -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 +29 -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 +64 -0
- package/dist/public/telemetry-acceptance.d.ts.map +1 -1
- package/dist/public/telemetry-acceptance.js +143 -7
- 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/diagnostic-usage.ts +59 -1
- 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/bootstrap.ts +78 -4
- 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/symbol-repair-plan.ts +3 -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 +52 -11
- package/src/public/skills/kibi-bootstrap/resources/bootstrap.md +12 -1
- package/src/public/skills/kibi-usage/SKILL.md +82 -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 +29 -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 +210 -7
- 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,846 @@
|
|
|
1
|
+
import { escapeAtom, normalizeEntityId, parseEntityFromList, parseListOfLists, parseTriples, } from "./prolog/codec.js";
|
|
2
|
+
import { entityRowsGoal } from "./public/operations/discovery-entities.js";
|
|
3
|
+
import { runOperationJsonQuery } from "./public/operations/prolog-json.js";
|
|
4
|
+
import { loadMarkdownBody } from "./search-ranking.js";
|
|
5
|
+
const CURRENT_REQ_STATUSES = new Set([
|
|
6
|
+
"open",
|
|
7
|
+
"in_progress",
|
|
8
|
+
"closed",
|
|
9
|
+
"active",
|
|
10
|
+
"approved",
|
|
11
|
+
]);
|
|
12
|
+
const SUPERSEDED_ADR_STATUSES = new Set([
|
|
13
|
+
"superseded",
|
|
14
|
+
"deprecated",
|
|
15
|
+
"rejected",
|
|
16
|
+
]);
|
|
17
|
+
const ANSWER_RELATIONSHIPS = [
|
|
18
|
+
"specified_by",
|
|
19
|
+
"verified_by",
|
|
20
|
+
"validates",
|
|
21
|
+
"implements",
|
|
22
|
+
"constrains",
|
|
23
|
+
"requires_property",
|
|
24
|
+
"requires_predicate",
|
|
25
|
+
"requires_rule",
|
|
26
|
+
"supersedes",
|
|
27
|
+
"relates_to",
|
|
28
|
+
"exempts",
|
|
29
|
+
];
|
|
30
|
+
const FACT_LINKS = new Set([
|
|
31
|
+
"constrains",
|
|
32
|
+
"requires_property",
|
|
33
|
+
"requires_predicate",
|
|
34
|
+
"requires_rule",
|
|
35
|
+
]);
|
|
36
|
+
/** Properties the answer reads from a linked entity. */
|
|
37
|
+
const ROW_KEYS = [
|
|
38
|
+
"id",
|
|
39
|
+
"title",
|
|
40
|
+
"status",
|
|
41
|
+
"fact_kind",
|
|
42
|
+
"approved_by",
|
|
43
|
+
"source",
|
|
44
|
+
"sourceFile",
|
|
45
|
+
];
|
|
46
|
+
// implements REQ-kibi-search-answer-layer-v2
|
|
47
|
+
export const SEARCH_ANSWER_LIMITS = {
|
|
48
|
+
seeds: 10,
|
|
49
|
+
governing: 5,
|
|
50
|
+
perRequirement: 4,
|
|
51
|
+
rationale: 3,
|
|
52
|
+
notGoverning: 5,
|
|
53
|
+
observations: 3,
|
|
54
|
+
/** Entity rows loaded per answer, the matched seeds included. */
|
|
55
|
+
entityLoads: 70,
|
|
56
|
+
maxBytes: 16_384,
|
|
57
|
+
/** Title length (characters) kept when an answer must shrink to fit. */
|
|
58
|
+
titleChars: 160,
|
|
59
|
+
/** Characters of an ADR decision kept as its excerpt. */
|
|
60
|
+
excerptChars: 280,
|
|
61
|
+
/** Characters of a witness or unknown detail kept. */
|
|
62
|
+
detailChars: 240,
|
|
63
|
+
};
|
|
64
|
+
function text(value) {
|
|
65
|
+
return typeof value === "string" ? value : value == null ? "" : String(value);
|
|
66
|
+
}
|
|
67
|
+
function unique(ids) {
|
|
68
|
+
return [...new Set(ids.filter((id) => id.length > 0))];
|
|
69
|
+
}
|
|
70
|
+
function brief(entity) {
|
|
71
|
+
const status = text(entity.status);
|
|
72
|
+
return {
|
|
73
|
+
id: text(entity.id),
|
|
74
|
+
title: text(entity.title),
|
|
75
|
+
...(status ? { status } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function edgesGoal(ids) {
|
|
79
|
+
const idList = ids.map((id) => `'${escapeAtom(id)}'`).join(",");
|
|
80
|
+
return `findall([Rel,From,To], (member(Rel, [${ANSWER_RELATIONSHIPS.join(",")}]), kb_relationship(Rel, From, To), (member(From, [${idList}]); member(To, [${idList}]))), Edges)`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The part of the KB graph the answer reads, fetched in batches: each
|
|
84
|
+
* `fetch` is one engine round trip for every id whose edges or row are not
|
|
85
|
+
* known yet. Answering used to cost one round trip per edge hop and per
|
|
86
|
+
* linked entity; on a journaled engine each costs a polling interval, which
|
|
87
|
+
* made a warm search take seconds.
|
|
88
|
+
*/
|
|
89
|
+
class AnswerGraph {
|
|
90
|
+
prolog;
|
|
91
|
+
rowBudget;
|
|
92
|
+
edgesById = new Map();
|
|
93
|
+
rows = new Map();
|
|
94
|
+
rowLoads = 0;
|
|
95
|
+
truncated = false;
|
|
96
|
+
constructor(prolog, rowBudget) {
|
|
97
|
+
this.prolog = prolog;
|
|
98
|
+
this.rowBudget = rowBudget;
|
|
99
|
+
}
|
|
100
|
+
seed(entity) {
|
|
101
|
+
const id = text(entity.id);
|
|
102
|
+
if (id)
|
|
103
|
+
this.rows.set(id, { ...entity });
|
|
104
|
+
}
|
|
105
|
+
row(id) {
|
|
106
|
+
return this.rows.get(id) ?? null;
|
|
107
|
+
}
|
|
108
|
+
/** Edges touching any of `ids` that were fetched, without duplicates. */
|
|
109
|
+
edgesOf(ids) {
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
const edges = [];
|
|
112
|
+
for (const id of ids) {
|
|
113
|
+
for (const edge of this.edgesById.get(id) ?? []) {
|
|
114
|
+
const key = `${edge.rel}|${edge.from}|${edge.to}`;
|
|
115
|
+
if (seen.has(key))
|
|
116
|
+
continue;
|
|
117
|
+
seen.add(key);
|
|
118
|
+
edges.push(edge);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return edges;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Fetch missing edges and rows in one query. `alsoGoal` is conjoined so a
|
|
125
|
+
* further lookup shares the round trip; its bindings are returned.
|
|
126
|
+
*/
|
|
127
|
+
async fetch(request) {
|
|
128
|
+
const edgeIds = unique(request.edges ?? []).filter((id) => !this.edgesById.has(id));
|
|
129
|
+
const wanted = unique(request.rows ?? []).filter((id) => !this.rows.has(id));
|
|
130
|
+
const room = Math.max(0, this.rowBudget - this.rowLoads);
|
|
131
|
+
if (wanted.length > room)
|
|
132
|
+
this.truncated = true;
|
|
133
|
+
const rowIds = wanted.slice(0, room);
|
|
134
|
+
const goals = [
|
|
135
|
+
...(edgeIds.length > 0 ? [edgesGoal(edgeIds)] : []),
|
|
136
|
+
...(rowIds.length > 0 ? [entityRowsGoal(rowIds, ROW_KEYS)] : []),
|
|
137
|
+
...(request.alsoGoal ? [request.alsoGoal] : []),
|
|
138
|
+
];
|
|
139
|
+
if (goals.length === 0)
|
|
140
|
+
return {};
|
|
141
|
+
const result = await this.prolog.query(goals.join(", "));
|
|
142
|
+
if (!result.success) {
|
|
143
|
+
// A failed edge lookup reads as no links, as before batching; a
|
|
144
|
+
// failed entity lookup must not silently drop requirements.
|
|
145
|
+
if (rowIds.length > 0 || request.alsoGoal) {
|
|
146
|
+
throw new Error(result.error || "Search answer graph query failed");
|
|
147
|
+
}
|
|
148
|
+
for (const id of edgeIds)
|
|
149
|
+
this.edgesById.set(id, []);
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
if (edgeIds.length > 0) {
|
|
153
|
+
for (const id of edgeIds)
|
|
154
|
+
this.edgesById.set(id, []);
|
|
155
|
+
for (const [rel, from, to] of parseTriples(result.bindings.Edges ?? "[]")) {
|
|
156
|
+
const edge = {
|
|
157
|
+
rel,
|
|
158
|
+
from: normalizeEntityId(from),
|
|
159
|
+
to: normalizeEntityId(to),
|
|
160
|
+
};
|
|
161
|
+
this.edgesById.get(edge.from)?.push(edge);
|
|
162
|
+
if (edge.to !== edge.from)
|
|
163
|
+
this.edgesById.get(edge.to)?.push(edge);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (rowIds.length > 0) {
|
|
167
|
+
this.rowLoads += rowIds.length;
|
|
168
|
+
for (const id of rowIds)
|
|
169
|
+
this.rows.set(id, null);
|
|
170
|
+
for (const row of parseListOfLists(result.bindings.Results ?? "[]").map(parseEntityFromList))
|
|
171
|
+
this.rows.set(normalizeEntityId(text(row.id)), row);
|
|
172
|
+
}
|
|
173
|
+
return result.bindings;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Tests that verify a scenario: `verified_by` from it or `validates` into it. */
|
|
177
|
+
function scenarioTests(scenarioId, edges) {
|
|
178
|
+
const tests = [];
|
|
179
|
+
for (const edge of edges) {
|
|
180
|
+
if (edge.rel === "verified_by" && edge.from === scenarioId)
|
|
181
|
+
tests.push(edge.to);
|
|
182
|
+
if (edge.rel === "validates" && edge.to === scenarioId)
|
|
183
|
+
tests.push(edge.from);
|
|
184
|
+
}
|
|
185
|
+
return tests;
|
|
186
|
+
}
|
|
187
|
+
/** Requirement ids a non-requirement entity points back to. */
|
|
188
|
+
function owningRequirements(id, type, edges) {
|
|
189
|
+
const owners = [];
|
|
190
|
+
for (const edge of edges) {
|
|
191
|
+
if (type === "scenario" && edge.rel === "specified_by" && edge.to === id)
|
|
192
|
+
owners.push(edge.from);
|
|
193
|
+
if (type === "test" && edge.rel === "verified_by" && edge.to === id)
|
|
194
|
+
owners.push(edge.from);
|
|
195
|
+
if (type === "test" && edge.rel === "validates" && edge.from === id)
|
|
196
|
+
owners.push(edge.to);
|
|
197
|
+
if (type === "symbol" && edge.rel === "implements" && edge.from === id)
|
|
198
|
+
owners.push(edge.to);
|
|
199
|
+
if (type === "fact" && FACT_LINKS.has(edge.rel) && edge.to === id)
|
|
200
|
+
owners.push(edge.from);
|
|
201
|
+
}
|
|
202
|
+
return owners;
|
|
203
|
+
}
|
|
204
|
+
/** Shorten to at most `maxChars` code points, ending with an ellipsis. */
|
|
205
|
+
function clip(value, maxChars) {
|
|
206
|
+
const chars = Array.from(value);
|
|
207
|
+
if (chars.length <= maxChars)
|
|
208
|
+
return value;
|
|
209
|
+
return `${chars.slice(0, Math.max(0, maxChars - 1)).join("")}…`;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The opening sentences of an ADR's Decision section (falling back to
|
|
213
|
+
* Rationale, then to the first prose paragraph), as plain text of at most
|
|
214
|
+
* `maxChars` characters.
|
|
215
|
+
*/
|
|
216
|
+
// implements REQ-kibi-search-answer-layer-v2
|
|
217
|
+
export function adrExcerpt(body, maxChars = SEARCH_ANSWER_LIMITS.excerptChars) {
|
|
218
|
+
const sections = new Map();
|
|
219
|
+
const preamble = [];
|
|
220
|
+
let current = preamble;
|
|
221
|
+
let fenced = false;
|
|
222
|
+
for (const line of body.split(/\r?\n/)) {
|
|
223
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
224
|
+
fenced = !fenced;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (fenced)
|
|
228
|
+
continue;
|
|
229
|
+
const heading = /^\s{0,3}#{1,6}\s+(.*?)\s*#*\s*$/.exec(line);
|
|
230
|
+
if (heading) {
|
|
231
|
+
const name = text(heading[1]).toLowerCase();
|
|
232
|
+
current = [];
|
|
233
|
+
if (!sections.has(name))
|
|
234
|
+
sections.set(name, current);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
current.push(line);
|
|
238
|
+
}
|
|
239
|
+
const named = (pattern) => [...sections.entries()].find(([name, lines]) => pattern.test(name) && lines.join("").trim() !== "")?.[1];
|
|
240
|
+
const lines = named(/^decision\b/) ??
|
|
241
|
+
named(/^rationale\b/) ??
|
|
242
|
+
(preamble.join("").trim() !== ""
|
|
243
|
+
? preamble
|
|
244
|
+
: [...sections.values()].find((section) => section.join("").trim()));
|
|
245
|
+
if (!lines)
|
|
246
|
+
return undefined;
|
|
247
|
+
const prose = lines
|
|
248
|
+
.map((line) => line
|
|
249
|
+
.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "")
|
|
250
|
+
.replace(/^\s*>\s?/, "")
|
|
251
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
252
|
+
.replace(/\*\*|__|`/g, "")
|
|
253
|
+
.trim())
|
|
254
|
+
.filter((line) => line !== "" && !/^\|/.test(line))
|
|
255
|
+
.join(" ")
|
|
256
|
+
.replace(/\s+/g, " ")
|
|
257
|
+
.trim();
|
|
258
|
+
if (!prose)
|
|
259
|
+
return undefined;
|
|
260
|
+
const sentences = prose.split(/(?<=[.!?])\s+/);
|
|
261
|
+
let excerpt = "";
|
|
262
|
+
for (const sentence of sentences) {
|
|
263
|
+
const next = `${excerpt} ${sentence.trim()}`.trim();
|
|
264
|
+
if (excerpt !== "" && Array.from(next).length > maxChars)
|
|
265
|
+
break;
|
|
266
|
+
excerpt = next;
|
|
267
|
+
}
|
|
268
|
+
return clip(excerpt, maxChars);
|
|
269
|
+
}
|
|
270
|
+
async function rationaleOf(row, workspaceRoot, bodies) {
|
|
271
|
+
const source = text(row.sourceFile) || text(row.source);
|
|
272
|
+
if (!source)
|
|
273
|
+
return brief(row);
|
|
274
|
+
let excerpt;
|
|
275
|
+
if (workspaceRoot !== undefined) {
|
|
276
|
+
let body = bodies.get(source);
|
|
277
|
+
if (body === undefined) {
|
|
278
|
+
body = loadMarkdownBody(source, workspaceRoot);
|
|
279
|
+
bodies.set(source, body);
|
|
280
|
+
}
|
|
281
|
+
const markdown = await body;
|
|
282
|
+
excerpt = markdown ? adrExcerpt(markdown) : undefined;
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
...brief(row),
|
|
286
|
+
source: source.split("#", 1)[0] ?? source,
|
|
287
|
+
...(excerpt ? { excerpt } : {}),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function records(value) {
|
|
291
|
+
return Array.isArray(value)
|
|
292
|
+
? value.filter((item) => item !== null && typeof item === "object" && !Array.isArray(item))
|
|
293
|
+
: [];
|
|
294
|
+
}
|
|
295
|
+
function strings(value) {
|
|
296
|
+
return Array.isArray(value)
|
|
297
|
+
? value.map((item) => normalizeEntityId(text(item))).filter(Boolean)
|
|
298
|
+
: [];
|
|
299
|
+
}
|
|
300
|
+
const FEASIBILITY_UNKNOWN_TEXT = {
|
|
301
|
+
no_assumptions: "expects success but assumes nothing, so its feasibility against current requirements is unknown",
|
|
302
|
+
unmatched_assumption: "expects success but an assumption is not constrained by any current requirement, so its feasibility is unknown",
|
|
303
|
+
incomparable_assumption: "expects success but an assumption cannot be compared with the requirement constraint (type or unit differs), so its feasibility is unknown",
|
|
304
|
+
contradictory_assumptions: "expects success but its assumptions contradict each other, so the scenario can never happen",
|
|
305
|
+
conflicting_requirements: "expects success but the requirements constraining it conflict with each other, so its feasibility is unknown",
|
|
306
|
+
undecided_rule: "expects success but a rule governing it can be neither satisfied nor refuted from its assumptions, so its feasibility is unknown",
|
|
307
|
+
undetermined_validity: "expects success but would conflict only with constraints whose validity window may not cover the scenario's time, so its feasibility is unknown",
|
|
308
|
+
};
|
|
309
|
+
/** Project the Prolog verdict of one requirement onto the answer fields. */
|
|
310
|
+
// implements REQ-kibi-search-answer-layer-v2
|
|
311
|
+
export function projectVerdict(raw, perRequirement = SEARCH_ANSWER_LIMITS.perRequirement) {
|
|
312
|
+
const detail = (value) => clip(text(value), SEARCH_ANSWER_LIMITS.detailChars);
|
|
313
|
+
const witnesses = [];
|
|
314
|
+
const unknowns = [];
|
|
315
|
+
if (raw === undefined || raw.error !== undefined) {
|
|
316
|
+
unknowns.push({
|
|
317
|
+
kind: "verdict_unavailable",
|
|
318
|
+
detail: detail(raw?.error !== undefined
|
|
319
|
+
? `The checks could not be evaluated for this requirement: ${text(raw.error)}`
|
|
320
|
+
: "The checks could not be evaluated for this requirement."),
|
|
321
|
+
entities: [],
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
for (const row of records(raw.contradictions)) {
|
|
326
|
+
const other = normalizeEntityId(text(row.with));
|
|
327
|
+
if (text(row.status) === "contradiction") {
|
|
328
|
+
witnesses.push({
|
|
329
|
+
check: "domain-contradictions",
|
|
330
|
+
status: "contradiction",
|
|
331
|
+
...(other ? { with: other } : {}),
|
|
332
|
+
facts: strings(row.facts),
|
|
333
|
+
detail: detail(row.reason),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
unknowns.push({
|
|
338
|
+
kind: "contradiction_unresolved",
|
|
339
|
+
detail: detail(row.reason),
|
|
340
|
+
entities: [other, ...strings(row.facts)].filter(Boolean),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const row of records(raw.scenarios)) {
|
|
345
|
+
const scenario = normalizeEntityId(text(row.scenario));
|
|
346
|
+
const outcome = text(row.outcome);
|
|
347
|
+
if (outcome === "infeasible") {
|
|
348
|
+
const forbidding = normalizeEntityId(text(row.requirement));
|
|
349
|
+
witnesses.push({
|
|
350
|
+
check: "scenario-feasibility",
|
|
351
|
+
status: "infeasible",
|
|
352
|
+
...(forbidding ? { with: forbidding } : {}),
|
|
353
|
+
scenario,
|
|
354
|
+
facts: [...strings(row.assumed), ...strings(row.fact)],
|
|
355
|
+
detail: detail(row.reason),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
else if (outcome === "unknown") {
|
|
359
|
+
const reason = text(row.reason);
|
|
360
|
+
unknowns.push({
|
|
361
|
+
kind: "feasibility_unknown",
|
|
362
|
+
detail: detail(`${scenario} ${FEASIBILITY_UNKNOWN_TEXT[reason] ?? `has unknown feasibility (${reason})`}`),
|
|
363
|
+
entities: [scenario, ...strings(row.facts)],
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
for (const row of records(raw.forbids)) {
|
|
368
|
+
witnesses.push({
|
|
369
|
+
check: "scenario-feasibility",
|
|
370
|
+
status: "infeasible",
|
|
371
|
+
scenario: normalizeEntityId(text(row.scenario)),
|
|
372
|
+
facts: [...strings(row.assumed), ...strings(row.fact)],
|
|
373
|
+
detail: detail(row.reason),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const inventory = records([raw.inventory])[0];
|
|
377
|
+
if (inventory !== undefined) {
|
|
378
|
+
for (const claim of records(inventory.unresolved)) {
|
|
379
|
+
unknowns.push({
|
|
380
|
+
kind: "unresolved_proposition",
|
|
381
|
+
detail: detail(`${text(claim.status)}: ${text(claim.claim)}`),
|
|
382
|
+
entities: [],
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
if (text(inventory.status) === "missing") {
|
|
386
|
+
unknowns.push({
|
|
387
|
+
kind: "analysis_incomplete",
|
|
388
|
+
detail: "The requirement's clause ledger (semantic inventory) is missing or incomplete, so contradiction analysis cannot cover all of its clauses.",
|
|
389
|
+
entities: [],
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const status = witnesses.some((witness) => witness.status === "contradiction")
|
|
395
|
+
? "contradiction"
|
|
396
|
+
: witnesses.length > 0
|
|
397
|
+
? "infeasible"
|
|
398
|
+
: unknowns.length > 0
|
|
399
|
+
? "unknown"
|
|
400
|
+
: "none";
|
|
401
|
+
// Contradictions first: they block regardless of scenarios.
|
|
402
|
+
witnesses.sort((left, right) => left.status === right.status ? 0 : left.status === "contradiction" ? -1 : 1);
|
|
403
|
+
return {
|
|
404
|
+
verdict: { status, witnesses: witnesses.slice(0, perRequirement) },
|
|
405
|
+
unknowns: unknowns.slice(0, perRequirement),
|
|
406
|
+
truncated: witnesses.length > perRequirement || unknowns.length > perRequirement,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const UNKNOWN_PROCEDURE = /existence_error|[Uu]nknown procedure|does not exist/;
|
|
410
|
+
function decodeJsonBinding(raw) {
|
|
411
|
+
if (raw === undefined)
|
|
412
|
+
return undefined;
|
|
413
|
+
let parsed = JSON.parse(raw);
|
|
414
|
+
if (typeof parsed === "string")
|
|
415
|
+
parsed = JSON.parse(parsed);
|
|
416
|
+
return parsed;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Rows the answer still needs plus the verdicts of the governing
|
|
420
|
+
* requirements, sharing one round trip while the discovery module is loaded;
|
|
421
|
+
* otherwise the module is loaded first. A verdict that cannot be computed
|
|
422
|
+
* leaves the answer standing with `verdict_unavailable` unknowns.
|
|
423
|
+
*/
|
|
424
|
+
async function fetchVerdicts(prolog, graph, rowIds, reqIds) {
|
|
425
|
+
const goal = `discovery:search_answer_verdicts_json([${reqIds.map((id) => `'${escapeAtom(id)}'`).join(",")}], JsonString)`;
|
|
426
|
+
try {
|
|
427
|
+
const bindings = await graph.fetch({ rows: rowIds, alsoGoal: goal });
|
|
428
|
+
return decodeJsonBinding(bindings.JsonString);
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
432
|
+
await graph.fetch({ rows: rowIds });
|
|
433
|
+
if (!UNKNOWN_PROCEDURE.test(message))
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
return await runOperationJsonQuery(prolog, "discovery.pl", goal, "Search answer verdicts");
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
export async function buildSearchAnswer(prolog, matches, options = {}) {
|
|
444
|
+
const limits = SEARCH_ANSWER_LIMITS;
|
|
445
|
+
const graph = new AnswerGraph(prolog, limits.entityLoads);
|
|
446
|
+
const seeds = matches.slice(0, limits.seeds);
|
|
447
|
+
let truncated = matches.length > limits.seeds;
|
|
448
|
+
const seedIds = seeds.map((match) => text(match.entity.id));
|
|
449
|
+
// Search rows are projected without fact_kind or approved_by, so the
|
|
450
|
+
// matched entities' own rows are loaded with their edges: notes are told
|
|
451
|
+
// apart from rules, and a matched exception keeps its approver. A match
|
|
452
|
+
// the store no longer holds keeps its search row.
|
|
453
|
+
await graph.fetch({ edges: seedIds, rows: seedIds });
|
|
454
|
+
for (const match of seeds)
|
|
455
|
+
if (graph.row(text(match.entity.id)) === null)
|
|
456
|
+
graph.seed(match.entity);
|
|
457
|
+
const seedEdges = graph.edgesOf(seedIds);
|
|
458
|
+
// Candidate requirements: matched requirements first, then requirements
|
|
459
|
+
// that own a matched scenario, test, symbol or fact.
|
|
460
|
+
const candidates = new Map();
|
|
461
|
+
const propose = (owner, score, via) => {
|
|
462
|
+
const previous = candidates.get(owner);
|
|
463
|
+
if (!previous || previous.score < score)
|
|
464
|
+
candidates.set(owner, { score, via });
|
|
465
|
+
};
|
|
466
|
+
const observations = [];
|
|
467
|
+
const rationaleRows = new Map();
|
|
468
|
+
// A matched test may verify a scenario rather than a requirement; its
|
|
469
|
+
// targets are resolved to their governing requirements after the loop.
|
|
470
|
+
const testTargets = [];
|
|
471
|
+
for (const match of seeds) {
|
|
472
|
+
const id = text(match.entity.id);
|
|
473
|
+
const entity = graph.row(id) ?? match.entity;
|
|
474
|
+
const type = text(entity.type || match.entity.type);
|
|
475
|
+
if (type === "req") {
|
|
476
|
+
if (!candidates.has(id))
|
|
477
|
+
candidates.set(id, { score: match.score, via: "matched" });
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (type === "adr") {
|
|
481
|
+
if (!SUPERSEDED_ADR_STATUSES.has(text(entity.status)))
|
|
482
|
+
rationaleRows.set(id, entity);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (type === "fact") {
|
|
486
|
+
const kind = text(entity.fact_kind);
|
|
487
|
+
if (kind === "observation" || kind === "meta") {
|
|
488
|
+
observations.push(brief(entity));
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const owners = owningRequirements(id, type, seedEdges);
|
|
493
|
+
if (type === "test") {
|
|
494
|
+
testTargets.push({ id, score: match.score, targets: owners });
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
for (const owner of owners)
|
|
498
|
+
propose(owner, match.score * 0.9, `${type} ${id}`);
|
|
499
|
+
}
|
|
500
|
+
const byScore = () => [...candidates.entries()]
|
|
501
|
+
.sort(([, left], [, right]) => right.score - left.score)
|
|
502
|
+
.map(([id]) => id);
|
|
503
|
+
const targetIds = unique(testTargets.flatMap((test) => test.targets));
|
|
504
|
+
// Test targets and the candidates found so far share one round trip.
|
|
505
|
+
await graph.fetch({
|
|
506
|
+
edges: [...targetIds, ...byScore()],
|
|
507
|
+
rows: byScore(),
|
|
508
|
+
});
|
|
509
|
+
const targetEdges = graph.edgesOf(targetIds);
|
|
510
|
+
for (const test of testTargets) {
|
|
511
|
+
for (const target of test.targets) {
|
|
512
|
+
// A target that some requirement specifies is a scenario: lift the
|
|
513
|
+
// test to that requirement. Anything else is a directly verified
|
|
514
|
+
// requirement (non-requirements are filtered when loaded).
|
|
515
|
+
const scenarioOwners = targetEdges
|
|
516
|
+
.filter((edge) => edge.rel === "specified_by" && edge.to === target)
|
|
517
|
+
.map((edge) => edge.from);
|
|
518
|
+
if (scenarioOwners.length === 0) {
|
|
519
|
+
propose(target, test.score * 0.9, `test ${test.id}`);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
for (const owner of scenarioOwners)
|
|
523
|
+
propose(owner, test.score * 0.85, `test ${test.id} via ${target}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const supersededBy = new Map();
|
|
527
|
+
for (const edge of seedEdges)
|
|
528
|
+
if (edge.rel === "supersedes")
|
|
529
|
+
supersededBy.set(edge.to, edge.from);
|
|
530
|
+
// A matched requirement that was superseded points at what governs now:
|
|
531
|
+
// follow the chain so the replacement is a candidate too.
|
|
532
|
+
let frontier = byScore();
|
|
533
|
+
for (let hop = 0; hop < 3 && frontier.length > 0; hop += 1) {
|
|
534
|
+
await graph.fetch({ edges: frontier, rows: frontier });
|
|
535
|
+
for (const edge of graph.edgesOf(frontier))
|
|
536
|
+
if (edge.rel === "supersedes")
|
|
537
|
+
supersededBy.set(edge.to, edge.from);
|
|
538
|
+
const next = [];
|
|
539
|
+
for (const id of frontier) {
|
|
540
|
+
const replacement = supersededBy.get(id);
|
|
541
|
+
const source = candidates.get(id);
|
|
542
|
+
if (replacement === undefined || source === undefined)
|
|
543
|
+
continue;
|
|
544
|
+
if (!candidates.has(replacement)) {
|
|
545
|
+
candidates.set(replacement, {
|
|
546
|
+
score: source.score * 0.95,
|
|
547
|
+
via: `supersedes ${id}`,
|
|
548
|
+
});
|
|
549
|
+
next.push(replacement);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
frontier = next;
|
|
553
|
+
}
|
|
554
|
+
// A replacement found on the last hop still needs its row and links, and
|
|
555
|
+
// a replacement that was itself superseded must not read as current.
|
|
556
|
+
if (frontier.length > 0) {
|
|
557
|
+
await graph.fetch({ edges: frontier, rows: frontier });
|
|
558
|
+
for (const edge of graph.edgesOf(frontier))
|
|
559
|
+
if (edge.rel === "supersedes")
|
|
560
|
+
supersededBy.set(edge.to, edge.from);
|
|
561
|
+
}
|
|
562
|
+
const chosen = [];
|
|
563
|
+
const notGoverning = [];
|
|
564
|
+
const ordered = [...candidates.entries()].sort(([leftId, left], [rightId, right]) => right.score - left.score || leftId.localeCompare(rightId));
|
|
565
|
+
for (const [id, candidate] of ordered) {
|
|
566
|
+
const entity = graph.row(id);
|
|
567
|
+
if (!entity || text(entity.type || "req") !== "req")
|
|
568
|
+
continue;
|
|
569
|
+
const replacement = supersededBy.get(id);
|
|
570
|
+
if (replacement !== undefined ||
|
|
571
|
+
!CURRENT_REQ_STATUSES.has(text(entity.status))) {
|
|
572
|
+
if (notGoverning.length < limits.notGoverning)
|
|
573
|
+
notGoverning.push({
|
|
574
|
+
...brief(entity),
|
|
575
|
+
...(replacement !== undefined ? { supersededBy: replacement } : {}),
|
|
576
|
+
});
|
|
577
|
+
else
|
|
578
|
+
truncated = true;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (chosen.length >= limits.governing) {
|
|
582
|
+
truncated = true;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
chosen.push({ id, entity, ...candidate });
|
|
586
|
+
}
|
|
587
|
+
// What each governing requirement links to, from the edges fetched above.
|
|
588
|
+
const firstOf = (ids) => {
|
|
589
|
+
const distinct = unique(ids);
|
|
590
|
+
if (distinct.length > limits.perRequirement)
|
|
591
|
+
truncated = true;
|
|
592
|
+
return distinct.slice(0, limits.perRequirement);
|
|
593
|
+
};
|
|
594
|
+
const plans = chosen.map((req) => {
|
|
595
|
+
const edges = graph.edgesOf([req.id]);
|
|
596
|
+
const from = (rel) => edges
|
|
597
|
+
.filter((edge) => edge.rel === rel && edge.from === req.id)
|
|
598
|
+
.map((edge) => edge.to);
|
|
599
|
+
const into = (rel) => edges
|
|
600
|
+
.filter((edge) => edge.rel === rel && edge.to === req.id)
|
|
601
|
+
.map((edge) => edge.from);
|
|
602
|
+
const scenarioIds = unique(from("specified_by"));
|
|
603
|
+
const traversed = firstOf(scenarioIds);
|
|
604
|
+
return {
|
|
605
|
+
req,
|
|
606
|
+
facts: firstOf(edges
|
|
607
|
+
.filter((edge) => FACT_LINKS.has(edge.rel) && edge.from === req.id)
|
|
608
|
+
.map((edge) => edge.to)),
|
|
609
|
+
scenarios: traversed,
|
|
610
|
+
directTests: unique([...from("verified_by"), ...into("validates")]),
|
|
611
|
+
related: firstOf([...from("relates_to"), ...into("relates_to")]),
|
|
612
|
+
exceptions: firstOf(into("exempts")),
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
await graph.fetch({
|
|
616
|
+
edges: plans.flatMap((plan) => plan.scenarios),
|
|
617
|
+
rows: plans.flatMap((plan) => [
|
|
618
|
+
...plan.facts,
|
|
619
|
+
...plan.scenarios,
|
|
620
|
+
...plan.related,
|
|
621
|
+
...plan.exceptions,
|
|
622
|
+
...plan.directTests.slice(0, limits.perRequirement),
|
|
623
|
+
]),
|
|
624
|
+
});
|
|
625
|
+
// Tests linked to the requirement itself come first, then tests that
|
|
626
|
+
// verify one of its scenarios; a test reached both ways counts as direct.
|
|
627
|
+
const testVias = plans.map((plan) => {
|
|
628
|
+
const testVia = new Map();
|
|
629
|
+
for (const testId of plan.directTests)
|
|
630
|
+
if (!testVia.has(testId))
|
|
631
|
+
testVia.set(testId, "direct");
|
|
632
|
+
const verification = graph.edgesOf(plan.scenarios);
|
|
633
|
+
for (const scenarioId of plan.scenarios)
|
|
634
|
+
for (const testId of scenarioTests(scenarioId, verification))
|
|
635
|
+
if (!testVia.has(testId))
|
|
636
|
+
testVia.set(testId, scenarioId);
|
|
637
|
+
return testVia;
|
|
638
|
+
});
|
|
639
|
+
const testIds = testVias.map((testVia) => firstOf([...testVia.keys()]));
|
|
640
|
+
const verdicts = await fetchVerdicts(prolog, graph, testIds.flat(), chosen.map((req) => req.id));
|
|
641
|
+
const verdictById = new Map(records(verdicts?.requirements).map((row) => [
|
|
642
|
+
normalizeEntityId(text(row.id)),
|
|
643
|
+
row,
|
|
644
|
+
]));
|
|
645
|
+
const bodies = new Map();
|
|
646
|
+
const rowsOf = (ids) => ids.map((id) => graph.row(id)).filter((row) => row !== null);
|
|
647
|
+
const governing = [];
|
|
648
|
+
for (const [index, plan] of plans.entries()) {
|
|
649
|
+
const { req } = plan;
|
|
650
|
+
const adrRows = rowsOf(plan.related).filter((row) => text(row.type) === "adr" &&
|
|
651
|
+
!SUPERSEDED_ADR_STATUSES.has(text(row.status)));
|
|
652
|
+
for (const adr of adrRows)
|
|
653
|
+
rationaleRows.set(text(adr.id), adr);
|
|
654
|
+
const projected = projectVerdict(verdicts === undefined ? undefined : verdictById.get(req.id));
|
|
655
|
+
if (projected.truncated)
|
|
656
|
+
truncated = true;
|
|
657
|
+
const testVia = testVias[index] ?? new Map();
|
|
658
|
+
governing.push({
|
|
659
|
+
...brief(req.entity),
|
|
660
|
+
score: req.score,
|
|
661
|
+
via: req.via,
|
|
662
|
+
facts: rowsOf(plan.facts).map((fact) => {
|
|
663
|
+
const factKind = text(fact.fact_kind);
|
|
664
|
+
return { ...brief(fact), ...(factKind ? { factKind } : {}) };
|
|
665
|
+
}),
|
|
666
|
+
scenarios: rowsOf(plan.scenarios).map(brief),
|
|
667
|
+
tests: rowsOf(testIds[index] ?? []).map((row) => ({
|
|
668
|
+
...brief(row),
|
|
669
|
+
via: testVia.get(text(row.id)) ?? "direct",
|
|
670
|
+
})),
|
|
671
|
+
adrs: adrRows.map(brief),
|
|
672
|
+
verdict: projected.verdict,
|
|
673
|
+
exceptions: rowsOf(plan.exceptions).map((row) => {
|
|
674
|
+
const approvedBy = text(row.approved_by).trim();
|
|
675
|
+
return { ...brief(row), ...(approvedBy ? { approvedBy } : {}) };
|
|
676
|
+
}),
|
|
677
|
+
unknowns: projected.unknowns,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
const rationaleList = [...rationaleRows.values()];
|
|
681
|
+
if (rationaleList.length > limits.rationale)
|
|
682
|
+
truncated = true;
|
|
683
|
+
if (observations.length > limits.observations)
|
|
684
|
+
truncated = true;
|
|
685
|
+
const rationale = await Promise.all(rationaleList
|
|
686
|
+
.slice(0, limits.rationale)
|
|
687
|
+
.map((row) => rationaleOf(row, options.workspaceRoot, bodies)));
|
|
688
|
+
const scope = {
|
|
689
|
+
branch: options.branch ?? null,
|
|
690
|
+
snapshotId: text(verdicts?.scope?.snapshotId) || "unknown",
|
|
691
|
+
syncedAt: typeof verdicts?.scope?.syncedAt === "string"
|
|
692
|
+
? verdicts.scope.syncedAt
|
|
693
|
+
: null,
|
|
694
|
+
};
|
|
695
|
+
const answer = {
|
|
696
|
+
version: "kibi.search-answer.v1",
|
|
697
|
+
governing,
|
|
698
|
+
rationale,
|
|
699
|
+
notGoverning,
|
|
700
|
+
observations: observations.slice(0, limits.observations),
|
|
701
|
+
scope,
|
|
702
|
+
truncated: truncated || graph.truncated,
|
|
703
|
+
note: governing.length === 0
|
|
704
|
+
? "No current requirement matched or owns a matched entity. Absence here is not evidence that nothing governs the change; refine the query or pass sourceLocations."
|
|
705
|
+
: "Governing requirements are current requirements matched by the query or linked to a matched entity. Links are discovery, not proof. A verdict lists the contradiction and scenario-feasibility witnesses that name the requirement; none means no check named it, not that it is proven. Use kb_check and kb_coverage for consistency and proof status.",
|
|
706
|
+
};
|
|
707
|
+
return fitSearchAnswer(answer, limits.maxBytes);
|
|
708
|
+
}
|
|
709
|
+
function byteSize(value) {
|
|
710
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
711
|
+
}
|
|
712
|
+
function clipTitle(entity, chars) {
|
|
713
|
+
return { ...entity, title: clip(entity.title, chars) };
|
|
714
|
+
}
|
|
715
|
+
function clipRationale(entity, chars) {
|
|
716
|
+
const clipped = clipTitle(entity, chars);
|
|
717
|
+
return entity.excerpt === undefined
|
|
718
|
+
? clipped
|
|
719
|
+
: {
|
|
720
|
+
...clipped,
|
|
721
|
+
excerpt: clip(entity.excerpt, SEARCH_ANSWER_LIMITS.excerptChars),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function withoutExcerpt(entity) {
|
|
725
|
+
if (entity.excerpt === undefined)
|
|
726
|
+
return entity;
|
|
727
|
+
const { excerpt: _excerpt, ...rest } = entity;
|
|
728
|
+
return rest;
|
|
729
|
+
}
|
|
730
|
+
const ANSWER_TRIM_ORDER = [
|
|
731
|
+
"observations",
|
|
732
|
+
"notGoverning",
|
|
733
|
+
"rationale",
|
|
734
|
+
];
|
|
735
|
+
const REQUIREMENT_TRIM_ORDER = [
|
|
736
|
+
"unknowns",
|
|
737
|
+
"exceptions",
|
|
738
|
+
"adrs",
|
|
739
|
+
"tests",
|
|
740
|
+
"scenarios",
|
|
741
|
+
"facts",
|
|
742
|
+
];
|
|
743
|
+
/**
|
|
744
|
+
* Hold the whole answer under `maxBytes`, marking it truncated when anything
|
|
745
|
+
* is cut. In order: long titles, excerpts and details are clipped; ADR
|
|
746
|
+
* excerpts are dropped, last rationale entry first; the least relevant
|
|
747
|
+
* governing requirements are dropped;
|
|
748
|
+
* list items are dropped (notes and side lists before the remaining
|
|
749
|
+
* requirement's own lists, its verdict witnesses last); then the remaining
|
|
750
|
+
* requirement's title and `via` are clipped. A verdict keeps its status when
|
|
751
|
+
* its witnesses are dropped. If even that cannot fit, the requirement itself
|
|
752
|
+
* is dropped.
|
|
753
|
+
*/
|
|
754
|
+
export function fitSearchAnswer(answer, maxBytes = SEARCH_ANSWER_LIMITS.maxBytes) {
|
|
755
|
+
let current = answer;
|
|
756
|
+
const over = () => byteSize(current) > maxBytes;
|
|
757
|
+
if (!over())
|
|
758
|
+
return current;
|
|
759
|
+
const titleChars = SEARCH_ANSWER_LIMITS.titleChars;
|
|
760
|
+
const detailChars = SEARCH_ANSWER_LIMITS.detailChars;
|
|
761
|
+
const clipAll = (rows) => rows.map((row) => clipTitle(row, titleChars));
|
|
762
|
+
current = {
|
|
763
|
+
...current,
|
|
764
|
+
truncated: true,
|
|
765
|
+
governing: current.governing.map((req) => ({
|
|
766
|
+
...clipTitle(req, titleChars),
|
|
767
|
+
facts: clipAll(req.facts),
|
|
768
|
+
scenarios: clipAll(req.scenarios),
|
|
769
|
+
tests: clipAll(req.tests),
|
|
770
|
+
adrs: clipAll(req.adrs),
|
|
771
|
+
verdict: {
|
|
772
|
+
...req.verdict,
|
|
773
|
+
witnesses: req.verdict.witnesses.map((witness) => ({
|
|
774
|
+
...witness,
|
|
775
|
+
detail: clip(witness.detail, detailChars),
|
|
776
|
+
})),
|
|
777
|
+
},
|
|
778
|
+
exceptions: clipAll(req.exceptions),
|
|
779
|
+
unknowns: req.unknowns.map((unknown) => ({
|
|
780
|
+
...unknown,
|
|
781
|
+
detail: clip(unknown.detail, detailChars),
|
|
782
|
+
})),
|
|
783
|
+
})),
|
|
784
|
+
rationale: current.rationale.map((adr) => clipRationale(adr, titleChars)),
|
|
785
|
+
notGoverning: clipAll(current.notGoverning),
|
|
786
|
+
observations: clipAll(current.observations),
|
|
787
|
+
};
|
|
788
|
+
// Excerpts are the most expendable text: drop them before any entry.
|
|
789
|
+
for (let index = current.rationale.length - 1; index >= 0 && over(); index -= 1) {
|
|
790
|
+
const rationale = [...current.rationale];
|
|
791
|
+
rationale[index] = withoutExcerpt(rationale[index]);
|
|
792
|
+
current = { ...current, rationale };
|
|
793
|
+
}
|
|
794
|
+
while (over() && current.governing.length > 1)
|
|
795
|
+
current = { ...current, governing: current.governing.slice(0, -1) };
|
|
796
|
+
while (over()) {
|
|
797
|
+
const side = ANSWER_TRIM_ORDER.find((key) => current[key].length > 0);
|
|
798
|
+
if (side) {
|
|
799
|
+
current = { ...current, [side]: current[side].slice(0, -1) };
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const [req] = current.governing;
|
|
803
|
+
if (!req)
|
|
804
|
+
break;
|
|
805
|
+
const own = REQUIREMENT_TRIM_ORDER.find((key) => req[key].length > 0);
|
|
806
|
+
if (own) {
|
|
807
|
+
current = {
|
|
808
|
+
...current,
|
|
809
|
+
governing: [{ ...req, [own]: req[own].slice(0, -1) }],
|
|
810
|
+
};
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (req.verdict.witnesses.length === 0)
|
|
814
|
+
break;
|
|
815
|
+
current = {
|
|
816
|
+
...current,
|
|
817
|
+
governing: [
|
|
818
|
+
{
|
|
819
|
+
...req,
|
|
820
|
+
verdict: {
|
|
821
|
+
...req.verdict,
|
|
822
|
+
witnesses: req.verdict.witnesses.slice(0, -1),
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
for (const field of ["title", "via"]) {
|
|
829
|
+
while (over()) {
|
|
830
|
+
const [req] = current.governing;
|
|
831
|
+
const chars = req ? Array.from(req[field]).length : 0;
|
|
832
|
+
if (!req || chars <= 1)
|
|
833
|
+
break;
|
|
834
|
+
// Each removed code point frees at least one byte; one more makes room
|
|
835
|
+
// for the ellipsis.
|
|
836
|
+
const keep = Math.max(1, chars - (byteSize(current) - maxBytes) - 2);
|
|
837
|
+
current = {
|
|
838
|
+
...current,
|
|
839
|
+
governing: [{ ...req, [field]: clip(req[field], keep) }],
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (over())
|
|
844
|
+
current = { ...current, governing: [] };
|
|
845
|
+
return current;
|
|
846
|
+
}
|