onto-mcp 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.onto/processes/reconstruct/actionable-ontology-seed-recomposition-design.md +447 -0
- package/.onto/processes/reconstruct/foundry-style-ontology-seed-contract.md +934 -0
- package/.onto/processes/reconstruct/reconstruct-boundary-contract.md +303 -725
- package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +1645 -0
- package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +26 -22
- package/.onto/processes/reconstruct/source-profile-contract.md +49 -23
- package/.onto/processes/reconstruct/source-profiles/code.md +6 -3
- package/.onto/processes/reconstruct/source-profiles/database.md +5 -2
- package/.onto/processes/reconstruct/source-profiles/document.md +5 -2
- package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +5 -4
- package/.onto/processes/review/review-execution-ux-contract.md +40 -0
- package/.onto/processes/shared/pipeline-execution-ledger-contract.md +26 -10
- package/.onto/processes/shared/target-material-kind-contract.md +29 -16
- package/AGENTS.md +6 -4
- package/README.md +135 -76
- package/dist/cli.js +8 -8
- package/dist/core-api/reconstruct-api.js +117 -31
- package/dist/core-api/review-api.js +47 -0
- package/dist/core-runtime/cli/codex-review-unit-executor.js +39 -2
- package/dist/core-runtime/cli/complete-review-session.js +2 -2
- package/dist/core-runtime/cli/mock-review-unit-executor.js +1 -1
- package/dist/core-runtime/cli/review-invoke.js +9 -9
- package/dist/core-runtime/cli/run-review-prompt-execution.js +39 -5
- package/dist/core-runtime/cli/spawn-watcher.js +266 -47
- package/dist/core-runtime/cli/start-review-session.js +3 -3
- package/dist/core-runtime/llm/llm-caller.js +11 -0
- package/dist/core-runtime/llm/llm-tool-loop.js +2 -0
- package/dist/core-runtime/observability/runtime-stream-observation.js +118 -0
- package/dist/core-runtime/onboard/cli-host.js +149 -0
- package/dist/core-runtime/onboard/host-target.js +22 -0
- package/dist/core-runtime/onboard/json-config-host.js +122 -0
- package/dist/core-runtime/onboard/path-scan.js +26 -0
- package/dist/core-runtime/onboard/prompt.js +51 -0
- package/dist/core-runtime/onboard/register.js +207 -0
- package/dist/core-runtime/onboard/types.js +27 -0
- package/dist/core-runtime/reconstruct/actionable-seed-validation.js +1777 -0
- package/dist/core-runtime/reconstruct/artifact-types.js +10 -4
- package/dist/core-runtime/reconstruct/contract-registry.js +623 -0
- package/dist/core-runtime/reconstruct/domain-id.js +10 -0
- package/dist/core-runtime/reconstruct/governing-snapshot.js +716 -0
- package/dist/core-runtime/reconstruct/material-profile-validation.js +191 -0
- package/dist/core-runtime/reconstruct/materialize-preparation.js +49 -11
- package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +269 -79
- package/dist/core-runtime/reconstruct/post-seed-validation.js +1194 -51
- package/dist/core-runtime/reconstruct/record.js +104 -20
- package/dist/core-runtime/reconstruct/run.js +2107 -413
- package/dist/core-runtime/reconstruct/seed-claim-projections.js +268 -0
- package/dist/core-runtime/reconstruct/source-profiles.js +93 -4
- package/dist/core-runtime/reconstruct/terminal-validation.js +807 -0
- package/dist/core-runtime/review/review-invocation-runner.js +4 -4
- package/dist/mcp/server.js +110 -38
- package/dist/mcp/tool-schemas.js +20 -6
- package/package.json +8 -17
- package/scripts/onto-review-watch.sh +486 -0
- package/scripts/onto-runtime-watch.sh +122 -0
- package/scripts/postinstall-hint.js +22 -0
- package/.onto/processes/reconstruct/top-level-concept-discovery-contract.md +0 -387
- package/dist/core-runtime/cli/bootstrap-review-binding.js +0 -186
- package/dist/core-runtime/cli/codex-nested-dispatch.test.js +0 -390
- package/dist/core-runtime/cli/codex-nested-teamlead-executor.test.js +0 -335
- package/dist/core-runtime/cli/coordinator-helpers.js +0 -583
- package/dist/core-runtime/cli/coordinator-state-machine-deliberation.test.js +0 -167
- package/dist/core-runtime/cli/coordinator-state-machine.js +0 -794
- package/dist/core-runtime/cli/e2e-codex-multi-agent-fixes.test.js +0 -615
- package/dist/core-runtime/cli/e2e-start-review-session.test.js +0 -312
- package/dist/core-runtime/cli/health.js +0 -44
- package/dist/core-runtime/cli/inline-http-review-unit-executor.test.js +0 -567
- package/dist/core-runtime/cli/materialize-review-execution-preparation.js +0 -104
- package/dist/core-runtime/cli/migrate-session-roots.js +0 -118
- package/dist/core-runtime/cli/repo-layout-migration-replace.smoke.test.js +0 -106
- package/dist/core-runtime/cli/review-invoke-auto-resolution.test.js +0 -268
- package/dist/core-runtime/cli/review-invoke-coordinator-topology.test.js +0 -136
- package/dist/core-runtime/cli/review-invoke-resolver-caching.test.js +0 -201
- package/dist/core-runtime/cli/review-invoke-topology-dispatch.test.js +0 -192
- package/dist/core-runtime/cli/session-root-guard.js +0 -168
- package/dist/core-runtime/cli/spawn-watcher.test.js +0 -457
- package/dist/core-runtime/cli/strip-wrapping-code-fence.test.js +0 -79
- package/dist/core-runtime/cli/teamcreate-lens-deliberation-executor.js +0 -412
- package/dist/core-runtime/cli/teamcreate-lens-deliberation-executor.test.js +0 -351
- package/dist/core-runtime/cli/topology-executor-mapping.js +0 -139
- package/dist/core-runtime/cli/topology-executor-mapping.test.js +0 -173
- package/dist/core-runtime/cli/write-review-interpretation.js +0 -81
- package/dist/core-runtime/config/onto-config-cli.js +0 -278
- package/dist/core-runtime/config/onto-config-key-path.js +0 -288
- package/dist/core-runtime/config/onto-config-key-path.test.js +0 -195
- package/dist/core-runtime/config/onto-config-preview.js +0 -108
- package/dist/core-runtime/config/onto-config-preview.test.js +0 -132
- package/dist/core-runtime/discovery/config-chain.js +0 -118
- package/dist/core-runtime/discovery/config-chain.test.js +0 -103
- package/dist/core-runtime/discovery/config-profile.js +0 -199
- package/dist/core-runtime/discovery/config-profile.test.js +0 -233
- package/dist/core-runtime/discovery/host-detection.test.js +0 -186
- package/dist/core-runtime/discovery/installation-paths.test.js +0 -65
- package/dist/core-runtime/discovery/lens-registry.test.js +0 -81
- package/dist/core-runtime/discovery/path-normalization.test.js +0 -22
- package/dist/core-runtime/discovery/plugin-path.js +0 -72
- package/dist/core-runtime/discovery/plugin-path.test.js +0 -95
- package/dist/core-runtime/evolve/adapters/code-product/compile/compile-defense.js +0 -344
- package/dist/core-runtime/evolve/adapters/code-product/compile/compile-defense.test.js +0 -915
- package/dist/core-runtime/evolve/adapters/code-product/compile/compile.js +0 -564
- package/dist/core-runtime/evolve/adapters/code-product/compile/compile.test.js +0 -708
- package/dist/core-runtime/evolve/adapters/code-product/parsers/brief-parser.js +0 -165
- package/dist/core-runtime/evolve/adapters/code-product/parsers/brief-parser.test.js +0 -227
- package/dist/core-runtime/evolve/adapters/code-product/validators/validate.js +0 -59
- package/dist/core-runtime/evolve/adapters/code-product/validators/validate.test.js +0 -205
- package/dist/core-runtime/evolve/adapters/methodology/adapter.js +0 -16
- package/dist/core-runtime/evolve/adapters/methodology/adapter.test.js +0 -9
- package/dist/core-runtime/evolve/adapters/methodology/perspectives/authority-consistency.js +0 -298
- package/dist/core-runtime/evolve/adapters/methodology/perspectives/authority-consistency.test.js +0 -70
- package/dist/core-runtime/evolve/adapters/methodology/scope-types/process.js +0 -46
- package/dist/core-runtime/evolve/adapters/methodology/scope-types/process.test.js +0 -73
- package/dist/core-runtime/evolve/adapters/registry.js +0 -47
- package/dist/core-runtime/evolve/adapters/registry.test.js +0 -67
- package/dist/core-runtime/evolve/cli.js +0 -256
- package/dist/core-runtime/evolve/commands/align.js +0 -194
- package/dist/core-runtime/evolve/commands/align.test.js +0 -82
- package/dist/core-runtime/evolve/commands/apply.js +0 -161
- package/dist/core-runtime/evolve/commands/apply.test.js +0 -138
- package/dist/core-runtime/evolve/commands/close.js +0 -39
- package/dist/core-runtime/evolve/commands/close.test.js +0 -99
- package/dist/core-runtime/evolve/commands/defer.js +0 -40
- package/dist/core-runtime/evolve/commands/defer.test.js +0 -134
- package/dist/core-runtime/evolve/commands/draft.js +0 -323
- package/dist/core-runtime/evolve/commands/draft.test.js +0 -178
- package/dist/core-runtime/evolve/commands/e2e-evolve-full-cycle.test.js +0 -208
- package/dist/core-runtime/evolve/commands/error-messages.js +0 -125
- package/dist/core-runtime/evolve/commands/error-messages.test.js +0 -167
- package/dist/core-runtime/evolve/commands/propose-align.js +0 -222
- package/dist/core-runtime/evolve/commands/propose-align.test.js +0 -136
- package/dist/core-runtime/evolve/commands/reconstruct.js +0 -330
- package/dist/core-runtime/evolve/commands/reconstruct.test.js +0 -278
- package/dist/core-runtime/evolve/commands/shared.js +0 -22
- package/dist/core-runtime/evolve/commands/stale-check.js +0 -103
- package/dist/core-runtime/evolve/commands/stale-check.test.js +0 -84
- package/dist/core-runtime/evolve/commands/start.js +0 -887
- package/dist/core-runtime/evolve/commands/start.test.js +0 -396
- package/dist/core-runtime/evolve/config/project-config.js +0 -99
- package/dist/core-runtime/evolve/config/project-config.test.js +0 -170
- package/dist/core-runtime/evolve/renderers/align-packet.js +0 -280
- package/dist/core-runtime/evolve/renderers/align-packet.test.js +0 -332
- package/dist/core-runtime/evolve/renderers/draft-packet.js +0 -303
- package/dist/core-runtime/evolve/renderers/draft-packet.test.js +0 -377
- package/dist/core-runtime/evolve/renderers/format.js +0 -5
- package/dist/core-runtime/evolve/renderers/scope-md.js +0 -237
- package/dist/core-runtime/evolve/renderers/scope-md.test.js +0 -306
- package/dist/core-runtime/govern/cli.js +0 -369
- package/dist/core-runtime/govern/cli.test.js +0 -314
- package/dist/core-runtime/govern/drift-engine.js +0 -103
- package/dist/core-runtime/govern/drift-engine.test.js +0 -319
- package/dist/core-runtime/govern/promote-principle.js +0 -206
- package/dist/core-runtime/govern/promote-principle.test.js +0 -368
- package/dist/core-runtime/govern/queue.js +0 -81
- package/dist/core-runtime/govern/types.js +0 -16
- package/dist/core-runtime/install/cli.js +0 -530
- package/dist/core-runtime/install/detect.js +0 -128
- package/dist/core-runtime/install/detect.test.js +0 -155
- package/dist/core-runtime/install/gitignore-update.js +0 -74
- package/dist/core-runtime/install/gitignore-update.test.js +0 -64
- package/dist/core-runtime/install/install-integration.test.js +0 -373
- package/dist/core-runtime/install/prompts.js +0 -389
- package/dist/core-runtime/install/prompts.test.js +0 -293
- package/dist/core-runtime/install/types.js +0 -26
- package/dist/core-runtime/install/validation.js +0 -295
- package/dist/core-runtime/install/validation.test.js +0 -313
- package/dist/core-runtime/install/writer.js +0 -254
- package/dist/core-runtime/install/writer.test.js +0 -218
- package/dist/core-runtime/learning/extractor.js +0 -461
- package/dist/core-runtime/learning/feedback.js +0 -179
- package/dist/core-runtime/learning/health-report.js +0 -165
- package/dist/core-runtime/learning/health-report.test.js +0 -169
- package/dist/core-runtime/learning/loader.js +0 -388
- package/dist/core-runtime/learning/loader.test.js +0 -102
- package/dist/core-runtime/learning/promote/apply-state.js +0 -240
- package/dist/core-runtime/learning/promote/audit-obligation.js +0 -195
- package/dist/core-runtime/learning/promote/collector.js +0 -432
- package/dist/core-runtime/learning/promote/degraded-state.js +0 -125
- package/dist/core-runtime/learning/promote/domain-doc-proposer.js +0 -166
- package/dist/core-runtime/learning/promote/e2e-promote.test.js +0 -6385
- package/dist/core-runtime/learning/promote/health-snapshot.js +0 -150
- package/dist/core-runtime/learning/promote/insight-reclassifier.js +0 -544
- package/dist/core-runtime/learning/promote/judgment-auditor.js +0 -517
- package/dist/core-runtime/learning/promote/panel-reviewer.js +0 -1158
- package/dist/core-runtime/learning/promote/promote-executor.js +0 -1675
- package/dist/core-runtime/learning/promote/promoter.js +0 -307
- package/dist/core-runtime/learning/promote/retirement.js +0 -122
- package/dist/core-runtime/learning/promote/types.js +0 -23
- package/dist/core-runtime/learning/prompt-sections.js +0 -51
- package/dist/core-runtime/learning/shared/artifact-registry-init.js +0 -45
- package/dist/core-runtime/learning/shared/artifact-registry.js +0 -254
- package/dist/core-runtime/learning/shared/audit-obligation-kernel.js +0 -73
- package/dist/core-runtime/learning/shared/audit-state.js +0 -99
- package/dist/core-runtime/learning/shared/duplicate-check.js +0 -28
- package/dist/core-runtime/learning/shared/llm-caller.js +0 -831
- package/dist/core-runtime/learning/shared/llm-caller.test.js +0 -601
- package/dist/core-runtime/learning/shared/llm-tool-loop.js +0 -393
- package/dist/core-runtime/learning/shared/mode.js +0 -25
- package/dist/core-runtime/learning/shared/paths.js +0 -84
- package/dist/core-runtime/learning/shared/paths.test.js +0 -79
- package/dist/core-runtime/learning/shared/patterns.js +0 -37
- package/dist/core-runtime/learning/shared/recoverability.js +0 -355
- package/dist/core-runtime/learning/shared/recovery-context.js +0 -374
- package/dist/core-runtime/learning/shared/scope.js +0 -1
- package/dist/core-runtime/learning/shared/semantic-classifier.js +0 -94
- package/dist/core-runtime/learning/shared/specs/apply-execution-state-spec.js +0 -42
- package/dist/core-runtime/learning/shared/specs/audit-state-spec.js +0 -37
- package/dist/core-runtime/learning/shared/specs/backup-metadata-spec.js +0 -39
- package/dist/core-runtime/learning/shared/specs/emergency-log-spec.js +0 -41
- package/dist/core-runtime/learning/shared/specs/layout-version-spec.js +0 -38
- package/dist/core-runtime/learning/shared/specs/promote-decisions-spec.js +0 -43
- package/dist/core-runtime/learning/shared/specs/promote-report-spec.js +0 -113
- package/dist/core-runtime/learning/shared/specs/prune-log-spec.js +0 -36
- package/dist/core-runtime/learning/shared/specs/recovery-resolution-spec.js +0 -48
- package/dist/core-runtime/learning/shared/specs/restore-manifest-spec.js +0 -43
- package/dist/core-runtime/learning/shared/specs/spec-helpers.js +0 -64
- package/dist/core-runtime/learning/usage-tracker.js +0 -190
- package/dist/core-runtime/learning/usage-tracker.test.js +0 -176
- package/dist/core-runtime/onboard/detect-review-axes.js +0 -122
- package/dist/core-runtime/onboard/detect-review-axes.test.js +0 -127
- package/dist/core-runtime/onboard/write-review-block.js +0 -188
- package/dist/core-runtime/onboard/write-review-block.test.js +0 -240
- package/dist/core-runtime/readers/brownfield-builder.js +0 -150
- package/dist/core-runtime/readers/brownfield-builder.test.js +0 -136
- package/dist/core-runtime/readers/code-chunk-collector.js +0 -53
- package/dist/core-runtime/readers/code-chunk-collector.test.js +0 -136
- package/dist/core-runtime/readers/file-utils.js +0 -240
- package/dist/core-runtime/readers/file-utils.test.js +0 -146
- package/dist/core-runtime/readers/lexicon-citation-check.js +0 -93
- package/dist/core-runtime/readers/lexicon-citation-check.test.js +0 -77
- package/dist/core-runtime/readers/mcp-figma.js +0 -30
- package/dist/core-runtime/readers/mcp-figma.test.js +0 -82
- package/dist/core-runtime/readers/mcp-generic.js +0 -31
- package/dist/core-runtime/readers/mcp-generic.test.js +0 -76
- package/dist/core-runtime/readers/ontology-index.js +0 -148
- package/dist/core-runtime/readers/ontology-index.test.js +0 -245
- package/dist/core-runtime/readers/ontology-query.js +0 -168
- package/dist/core-runtime/readers/ontology-query.test.js +0 -311
- package/dist/core-runtime/readers/ontology-resolve.js +0 -48
- package/dist/core-runtime/readers/ontology-resolve.test.js +0 -48
- package/dist/core-runtime/readers/patterns/index.js +0 -7
- package/dist/core-runtime/readers/review-log.js +0 -213
- package/dist/core-runtime/readers/review-log.test.js +0 -313
- package/dist/core-runtime/readers/scan-local.js +0 -102
- package/dist/core-runtime/readers/scan-local.test.js +0 -102
- package/dist/core-runtime/readers/scan-tarball.js +0 -121
- package/dist/core-runtime/readers/scan-tarball.test.js +0 -283
- package/dist/core-runtime/readers/scan-vault.js +0 -34
- package/dist/core-runtime/readers/scan-vault.test.js +0 -81
- package/dist/core-runtime/readers/types.js +0 -42
- package/dist/core-runtime/readers/types.test.js +0 -94
- package/dist/core-runtime/readers/viewpoint-collectors.js +0 -229
- package/dist/core-runtime/reconstruct/seed-candidate-validation.js +0 -385
- package/dist/core-runtime/review/citation-audit.test.js +0 -165
- package/dist/core-runtime/review/execution-plan-resolver.js +0 -247
- package/dist/core-runtime/review/execution-plan-resolver.test.js +0 -243
- package/dist/core-runtime/review/execution-topology-resolver-axis-first.test.js +0 -246
- package/dist/core-runtime/review/execution-topology-resolver.js +0 -401
- package/dist/core-runtime/review/execution-topology-resolver.test.js +0 -315
- package/dist/core-runtime/review/inline-context-embedder.test.js +0 -154
- package/dist/core-runtime/review/legacy-mode-policy.js +0 -88
- package/dist/core-runtime/review/materializers-effort-persist.test.js +0 -79
- package/dist/core-runtime/review/ontology-path-classifier.js +0 -179
- package/dist/core-runtime/review/ontology-path-classifier.test.js +0 -216
- package/dist/core-runtime/review/packet-boundary-policy.test.js +0 -107
- package/dist/core-runtime/review/participating-lens-paths.test.js +0 -73
- package/dist/core-runtime/review/review-config-legacy-translate.js +0 -244
- package/dist/core-runtime/review/review-config-legacy-translate.test.js +0 -161
- package/dist/core-runtime/review/review-config-validator.js +0 -289
- package/dist/core-runtime/review/review-config-validator.test.js +0 -236
- package/dist/core-runtime/review/shape-pipeline-audit.test.js +0 -311
- package/dist/core-runtime/review/shape-to-topology-id.js +0 -117
- package/dist/core-runtime/review/shape-to-topology-id.test.js +0 -132
- package/dist/core-runtime/review/topology-shape-derivation.js +0 -155
- package/dist/core-runtime/review/topology-shape-derivation.test.js +0 -195
- package/dist/core-runtime/scope-runtime/constants.js +0 -12
- package/dist/core-runtime/scope-runtime/constraint-pool.js +0 -166
- package/dist/core-runtime/scope-runtime/constraint-pool.test.js +0 -674
- package/dist/core-runtime/scope-runtime/domain-validation-log.js +0 -135
- package/dist/core-runtime/scope-runtime/domain-validation-log.test.js +0 -156
- package/dist/core-runtime/scope-runtime/eval-persistence.js +0 -65
- package/dist/core-runtime/scope-runtime/eval-persistence.test.js +0 -84
- package/dist/core-runtime/scope-runtime/event-pipeline.js +0 -64
- package/dist/core-runtime/scope-runtime/event-pipeline.test.js +0 -450
- package/dist/core-runtime/scope-runtime/event-store.js +0 -39
- package/dist/core-runtime/scope-runtime/event-store.test.js +0 -95
- package/dist/core-runtime/scope-runtime/gate-guard.js +0 -348
- package/dist/core-runtime/scope-runtime/gate-guard.test.js +0 -1047
- package/dist/core-runtime/scope-runtime/hash.js +0 -4
- package/dist/core-runtime/scope-runtime/hash.test.js +0 -33
- package/dist/core-runtime/scope-runtime/id.js +0 -4
- package/dist/core-runtime/scope-runtime/id.test.js +0 -17
- package/dist/core-runtime/scope-runtime/reducer.js +0 -297
- package/dist/core-runtime/scope-runtime/reducer.test.js +0 -759
- package/dist/core-runtime/scope-runtime/scope-manager.js +0 -161
- package/dist/core-runtime/scope-runtime/state-machine.js +0 -309
- package/dist/core-runtime/scope-runtime/state-machine.test.js +0 -704
- package/dist/core-runtime/scope-runtime/types.js +0 -116
- package/dist/core-runtime/scope-runtime/types.test.js +0 -69
- package/dist/core-runtime/translate/render-for-user.js +0 -169
- package/dist/core-runtime/translate/render-for-user.test.js +0 -122
- package/dist/providers/capability-contract.js +0 -1
|
@@ -1,1158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 3 Promote — Panel Reviewer (Step 8a).
|
|
3
|
-
*
|
|
4
|
-
* Design authority:
|
|
5
|
-
* - learn-phase3-design-v4.md DD-2 (3-agent panel composition + 축소 규칙)
|
|
6
|
-
* - learn-phase3-design-v5.md DD-7 (validator criteria 1~5)
|
|
7
|
-
* - learn-phase3-design-v4.md DD-7 (array validator signature)
|
|
8
|
-
* - learn-phase3-design-v4.md DD-12 (hard gate: valid_member_count < 2)
|
|
9
|
-
* - learn-phase3-design-v2.md DD-2 (per-member LLM call, API-based)
|
|
10
|
-
* - .onto/processes/learn/promote.md Step 3 (criteria 1~6 definitions)
|
|
11
|
-
*
|
|
12
|
-
* Responsibility:
|
|
13
|
-
* - DD-2 Panel composition: originator + axiology + auto_selected
|
|
14
|
-
* (축소 규칙: 관련 agent 부재 / 원본==auto_selected → 2-agent)
|
|
15
|
-
* - Per-member LLM call (1 batch for all candidates per member) via
|
|
16
|
-
* shared/llm-caller.ts. 1회 retry on validator failure.
|
|
17
|
-
* - DD-7 validator: criteria 1~5 array length + judgment coherence
|
|
18
|
-
* + duplicate_of coherence + consolidation_recommendation coherence
|
|
19
|
-
* - DD-12 hard gate: `consensus = panel_minimum_unmet` when valid
|
|
20
|
-
* member count drops below 2
|
|
21
|
-
* - Consensus aggregation: 3/3 / 2/3 / defer / reject / split
|
|
22
|
-
*
|
|
23
|
-
* Scope boundary:
|
|
24
|
-
* - Criteria 1~5 only. Criterion 6 (cross-agent dedup) is a separate
|
|
25
|
-
* single-agent sequential pass in a sibling helper.
|
|
26
|
-
* - Phase A (source-read-only). No mutation. No state persistence.
|
|
27
|
-
*
|
|
28
|
-
* Failure model:
|
|
29
|
-
* - LLM call failure → member becomes unreachable, consensus denominator
|
|
30
|
-
* shrinks dynamically. If every member is unreachable or
|
|
31
|
-
* contract_invalid → panel_minimum_unmet (DD-12).
|
|
32
|
-
*/
|
|
33
|
-
import crypto from "node:crypto";
|
|
34
|
-
import fs from "node:fs";
|
|
35
|
-
import os from "node:os";
|
|
36
|
-
import path from "node:path";
|
|
37
|
-
import { callLlm, hashPrompt } from "../shared/llm-caller.js";
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Panel composition — DD-2
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
/**
|
|
42
|
-
* Canonical axiology agent id. Mirrors the axiology learning file at
|
|
43
|
-
* `~/.onto/learnings/axiology.md` and the `axiology` lens in the review
|
|
44
|
-
* runtime. Exported so tests and higher-level helpers can reference it.
|
|
45
|
-
*/
|
|
46
|
-
export const AXIOLOGY_AGENT_ID = "axiology";
|
|
47
|
-
/**
|
|
48
|
-
* Enumerate known agent ids by listing `{ontoHome}/learnings/*.md`.
|
|
49
|
-
*
|
|
50
|
-
* The filename (without `.md`) is the agent id. We keep this derivation local
|
|
51
|
-
* to panel-reviewer so the collector's own path handling is not spilled into
|
|
52
|
-
* the public API — both modules agree on the `<home>/.onto/learnings/` layout
|
|
53
|
-
* via shared/paths.ts conventions.
|
|
54
|
-
*/
|
|
55
|
-
function listKnownAgents(ontoHome) {
|
|
56
|
-
const home = ontoHome ?? os.homedir();
|
|
57
|
-
const dir = path.join(home, ".onto", "learnings");
|
|
58
|
-
if (!fs.existsSync(dir))
|
|
59
|
-
return [];
|
|
60
|
-
return fs
|
|
61
|
-
.readdirSync(dir)
|
|
62
|
-
.filter((f) => f.endsWith(".md"))
|
|
63
|
-
.map((f) => f.slice(0, -3))
|
|
64
|
-
.map((id) => (id.startsWith("onto_") ? id.slice(5) : id))
|
|
65
|
-
.sort();
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Compose the 3-agent panel for a given candidate.
|
|
69
|
-
*
|
|
70
|
-
* DD-2 rules:
|
|
71
|
-
* 1. Panel member 1: originator (candidate.agent_id)
|
|
72
|
-
* 2. Panel member 2: axiology (always)
|
|
73
|
-
* 3. Panel member 3: auto_selected (first known agent that is neither
|
|
74
|
-
* originator nor axiology). If none can be selected, degrade to
|
|
75
|
-
* 2-agent.
|
|
76
|
-
*/
|
|
77
|
-
export function composePanel(candidate, config = {}) {
|
|
78
|
-
const members = [];
|
|
79
|
-
members.push({
|
|
80
|
-
agent_id: candidate.agent_id,
|
|
81
|
-
role: "originator",
|
|
82
|
-
reachable: true,
|
|
83
|
-
});
|
|
84
|
-
const axiologySelf = candidate.agent_id === AXIOLOGY_AGENT_ID;
|
|
85
|
-
if (!axiologySelf) {
|
|
86
|
-
members.push({
|
|
87
|
-
agent_id: AXIOLOGY_AGENT_ID,
|
|
88
|
-
role: "axiology",
|
|
89
|
-
reachable: true,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
// Auto-selected: first known lens that isn't originator or axiology.
|
|
93
|
-
const candidates = listKnownAgents(config.ontoHome).filter((id) => id !== candidate.agent_id && id !== AXIOLOGY_AGENT_ID);
|
|
94
|
-
if (candidates.length > 0) {
|
|
95
|
-
members.push({
|
|
96
|
-
agent_id: candidates[0],
|
|
97
|
-
role: "auto_selected",
|
|
98
|
-
reachable: true,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
// If no auto_selected agent exists we degrade gracefully to 2-agent.
|
|
102
|
-
// If the originator IS axiology we also naturally fall back.
|
|
103
|
-
return members;
|
|
104
|
-
}
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
// Candidate identification
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
/**
|
|
109
|
-
* Stable identifier for a panel candidate. Uses learning_id when present so
|
|
110
|
-
* Phase 2-written items are addressable by their durable id. Falls back to a
|
|
111
|
-
* short content hash for legacy items — the hash is stable across runs
|
|
112
|
-
* because it's derived from raw_line only.
|
|
113
|
-
*/
|
|
114
|
-
export function candidateIdOf(item) {
|
|
115
|
-
if (item.learning_id)
|
|
116
|
-
return item.learning_id;
|
|
117
|
-
return crypto
|
|
118
|
-
.createHash("sha256")
|
|
119
|
-
.update(item.raw_line)
|
|
120
|
-
.digest("hex")
|
|
121
|
-
.slice(0, 12);
|
|
122
|
-
}
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// Prompt building
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
const PANEL_SYSTEM_PROMPT_TEMPLATE = `You are reviewing promotion candidates for a learning management system as an expert in the role of {ROLE_AGENT_ID} ({ROLE_LABEL}).
|
|
127
|
-
|
|
128
|
-
Your task is to evaluate each candidate learning against 5 criteria AND recommend an axis tag adjustment. Output ONE JSON object per candidate in an "items" array. NO markdown fences, NO commentary, JSON only.
|
|
129
|
-
|
|
130
|
-
Criteria (.onto/processes/learn/promote.md):
|
|
131
|
-
1. Generalizability — is it valid across projects, or only in this project?
|
|
132
|
-
2. Accuracy — is it based on facts or a coincidence from a unique situation?
|
|
133
|
-
3. Contradiction handling — if it contradicts an existing global entry, which is more correct?
|
|
134
|
-
4. Axis tag appropriateness — use the 2+1 stage test on the candidate's tags.
|
|
135
|
-
5. Deduplication vs global — is this a domain variant of an existing principle?
|
|
136
|
-
|
|
137
|
-
For each candidate, return:
|
|
138
|
-
{
|
|
139
|
-
"candidate_id": "<the id you were given>",
|
|
140
|
-
"verdict": "promote" | "defer" | "reject",
|
|
141
|
-
"criteria": [
|
|
142
|
-
{"criterion": 1, "judgment": "yes" | "no" | "uncertain", "reasoning": "<one sentence>"},
|
|
143
|
-
{"criterion": 2, "judgment": "yes" | "no" | "uncertain", "reasoning": "<one sentence>"},
|
|
144
|
-
{"criterion": 3, "judgment": "yes" | "no" | "uncertain", "reasoning": "<one sentence>"},
|
|
145
|
-
{"criterion": 4, "judgment": "yes" | "no" | "uncertain", "reasoning": "<one sentence>"},
|
|
146
|
-
{"criterion": 5, "judgment": "yes" | "no" | "uncertain", "reasoning": "<one sentence>"}
|
|
147
|
-
],
|
|
148
|
-
"axis_tag_recommendation": "retain" | "add_methodology" | "remove_methodology" | "modify" | "no_recommendation",
|
|
149
|
-
"axis_tag_note": "<brief rationale>",
|
|
150
|
-
"contradiction_resolution": "replace" | "defer" | "n/a",
|
|
151
|
-
"reason": "<one-sentence summary of the verdict>"
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
Coherence rules (your output will be rejected if violated):
|
|
155
|
-
- criteria array MUST contain exactly 5 entries, one per criterion 1..5
|
|
156
|
-
- If every criterion 1..3 judgment is "yes" and criterion 4 is "yes"/"uncertain" and criterion 5 is "yes" (no duplicate), the verdict should be "promote"
|
|
157
|
-
- If any criterion 1..3 judgment is "no", the verdict MUST NOT be "promote"
|
|
158
|
-
- If criterion 5 judgment is "no" (is a duplicate of existing), the verdict MUST NOT be "promote" — recommend defer or reject
|
|
159
|
-
- If you provide a consolidation recommendation (via reason), the verdict MUST NOT be "promote"
|
|
160
|
-
`;
|
|
161
|
-
const PANEL_USER_PROMPT_TEMPLATE = `Review the promotion candidates below against the existing global learnings and return ONE JSON object with an "items" array — one entry per candidate.
|
|
162
|
-
|
|
163
|
-
[Candidates] {CANDIDATE_COUNT}
|
|
164
|
-
{CANDIDATE_BLOCK}
|
|
165
|
-
|
|
166
|
-
[Existing Global Learnings] {GLOBAL_COUNT}
|
|
167
|
-
{GLOBAL_BLOCK}
|
|
168
|
-
|
|
169
|
-
Respond ONLY with valid JSON shaped as:
|
|
170
|
-
{"items":[{...}, {...}]}
|
|
171
|
-
`;
|
|
172
|
-
function formatCandidate(candidate, index) {
|
|
173
|
-
const id = candidateIdOf(candidate);
|
|
174
|
-
const tags = candidate.applicability_tags.join(" ");
|
|
175
|
-
const role = candidate.role ?? "<no-role>";
|
|
176
|
-
return `${index + 1}. candidate_id=${id} type=${candidate.type} tags=[${tags}] role=${role} agent=${candidate.agent_id}\n content: ${candidate.content}`;
|
|
177
|
-
}
|
|
178
|
-
function formatGlobal(item, index) {
|
|
179
|
-
const tags = item.applicability_tags.join(" ");
|
|
180
|
-
const role = item.role ?? "<no-role>";
|
|
181
|
-
return `${index + 1}. [${item.type}] tags=[${tags}] role=${role} agent=${item.agent_id}\n content: ${item.content}`;
|
|
182
|
-
}
|
|
183
|
-
export function buildPanelPrompt(config) {
|
|
184
|
-
const maxGlobal = config.maxGlobalItems ?? 80;
|
|
185
|
-
const sortedGlobals = [...config.globalItems]
|
|
186
|
-
.sort((a, b) => {
|
|
187
|
-
const pathCmp = a.source_path.localeCompare(b.source_path);
|
|
188
|
-
if (pathCmp !== 0)
|
|
189
|
-
return pathCmp;
|
|
190
|
-
return a.line_number - b.line_number;
|
|
191
|
-
})
|
|
192
|
-
.slice(0, maxGlobal);
|
|
193
|
-
const candidateBlock = config.candidates
|
|
194
|
-
.map((c, i) => formatCandidate(c, i))
|
|
195
|
-
.join("\n");
|
|
196
|
-
const globalBlock = sortedGlobals
|
|
197
|
-
.map((g, i) => formatGlobal(g, i))
|
|
198
|
-
.join("\n");
|
|
199
|
-
let system_prompt = PANEL_SYSTEM_PROMPT_TEMPLATE.replace("{ROLE_AGENT_ID}", config.member.agent_id).replace("{ROLE_LABEL}", config.member.role);
|
|
200
|
-
if (config.retryFeedback && config.retryFeedback.length > 0) {
|
|
201
|
-
system_prompt +=
|
|
202
|
-
"\nPrevious attempt was rejected. Validator feedback:\n" +
|
|
203
|
-
config.retryFeedback.map((f) => ` - ${f}`).join("\n") +
|
|
204
|
-
"\nFix these issues and respond again.";
|
|
205
|
-
}
|
|
206
|
-
const user_prompt = PANEL_USER_PROMPT_TEMPLATE.replace("{CANDIDATE_COUNT}", String(config.candidates.length))
|
|
207
|
-
.replace("{CANDIDATE_BLOCK}", candidateBlock || "(none)")
|
|
208
|
-
.replace("{GLOBAL_COUNT}", String(sortedGlobals.length))
|
|
209
|
-
.replace("{GLOBAL_BLOCK}", globalBlock || "(none)");
|
|
210
|
-
return {
|
|
211
|
-
system_prompt,
|
|
212
|
-
user_prompt,
|
|
213
|
-
prompt_hash: hashPrompt(system_prompt + "\n" + user_prompt),
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Extract the first JSON object from an LLM response string.
|
|
218
|
-
*
|
|
219
|
-
* Models sometimes wrap JSON in markdown fences despite instructions;
|
|
220
|
-
* stripping triple backticks is enough in practice. If parsing fails we
|
|
221
|
-
* surface the error — the caller decides whether to retry.
|
|
222
|
-
*/
|
|
223
|
-
function parsePanelJson(text) {
|
|
224
|
-
let cleaned = text.trim();
|
|
225
|
-
if (cleaned.startsWith("```")) {
|
|
226
|
-
cleaned = cleaned.replace(/^```(?:json)?\s*/, "").replace(/```\s*$/, "");
|
|
227
|
-
}
|
|
228
|
-
const parsed = JSON.parse(cleaned);
|
|
229
|
-
if (!parsed || !Array.isArray(parsed.items)) {
|
|
230
|
-
throw new Error(`panel response missing "items" array (got keys: ${Object.keys(parsed ?? {}).join(",")})`);
|
|
231
|
-
}
|
|
232
|
-
return { items: parsed.items };
|
|
233
|
-
}
|
|
234
|
-
const VALID_VERDICTS = ["promote", "defer", "reject"];
|
|
235
|
-
const VALID_AXIS_RECOS = [
|
|
236
|
-
"retain",
|
|
237
|
-
"add_methodology",
|
|
238
|
-
"remove_methodology",
|
|
239
|
-
"modify",
|
|
240
|
-
"no_recommendation",
|
|
241
|
-
];
|
|
242
|
-
function validateCriteriaArray(criteria) {
|
|
243
|
-
const failures = [];
|
|
244
|
-
if (criteria.length !== 5) {
|
|
245
|
-
failures.push(`criteria array length ${criteria.length}, expected exactly 5`);
|
|
246
|
-
return failures;
|
|
247
|
-
}
|
|
248
|
-
const seen = new Set();
|
|
249
|
-
for (const c of criteria) {
|
|
250
|
-
if (c.criterion < 1 || c.criterion > 5) {
|
|
251
|
-
failures.push(`criterion ${c.criterion} out of range 1..5`);
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (seen.has(c.criterion)) {
|
|
255
|
-
failures.push(`criterion ${c.criterion} repeated`);
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
seen.add(c.criterion);
|
|
259
|
-
if (!["yes", "no", "uncertain"].includes(c.judgment)) {
|
|
260
|
-
failures.push(`criterion ${c.criterion}: invalid judgment "${c.judgment}"`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
for (let i = 1; i <= 5; i++) {
|
|
264
|
-
if (!seen.has(i))
|
|
265
|
-
failures.push(`criterion ${i} missing`);
|
|
266
|
-
}
|
|
267
|
-
return failures;
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* DD-7 per-member validator. Applies criteria array structural checks plus
|
|
271
|
-
* judgment coherence checks (criteria 1~3 vs verdict, criterion 5 vs
|
|
272
|
-
* verdict).
|
|
273
|
-
*/
|
|
274
|
-
export function validatePanelMemberReview(review) {
|
|
275
|
-
const failures = [];
|
|
276
|
-
if (!VALID_VERDICTS.includes(review.verdict)) {
|
|
277
|
-
failures.push(`verdict "${review.verdict}" not in ${VALID_VERDICTS.join("|")}`);
|
|
278
|
-
}
|
|
279
|
-
if (!VALID_AXIS_RECOS.includes(review.axis_tag_recommendation)) {
|
|
280
|
-
failures.push(`axis_tag_recommendation "${review.axis_tag_recommendation}" not valid`);
|
|
281
|
-
}
|
|
282
|
-
failures.push(...validateCriteriaArray(review.criteria));
|
|
283
|
-
if (failures.length > 0) {
|
|
284
|
-
return { passed: false, failures };
|
|
285
|
-
}
|
|
286
|
-
// Criteria 1~5 vs verdict coherence (M-C fix: c4 is now in the all-yes
|
|
287
|
-
// gate, matching the prompt at line 196 which says promote is required when
|
|
288
|
-
// criteria 1..3 are yes AND criterion 4 is yes/uncertain AND criterion 5 is
|
|
289
|
-
// yes. Without c4 in the check, the validator wrongly rejected coherent
|
|
290
|
-
// responses like "c1-3 yes, c4 no, c5 yes, verdict defer" — the model
|
|
291
|
-
// correctly avoided promoting an item with bad axis tags, but the validator
|
|
292
|
-
// demanded promote anyway).
|
|
293
|
-
const c = new Map(review.criteria.map((x) => [x.criterion, x.judgment]));
|
|
294
|
-
const c1 = c.get(1);
|
|
295
|
-
const c2 = c.get(2);
|
|
296
|
-
const c3 = c.get(3);
|
|
297
|
-
const c4 = c.get(4);
|
|
298
|
-
const c5 = c.get(5);
|
|
299
|
-
const c4PassesGate = c4 === "yes" || c4 === "uncertain";
|
|
300
|
-
if (c1 === "yes" &&
|
|
301
|
-
c2 === "yes" &&
|
|
302
|
-
c3 === "yes" &&
|
|
303
|
-
c4PassesGate &&
|
|
304
|
-
c5 === "yes") {
|
|
305
|
-
if (review.verdict !== "promote") {
|
|
306
|
-
failures.push("criteria 1,2,3 yes + c4 yes/uncertain + c5 yes but verdict is not promote");
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if ((c1 === "no" || c2 === "no" || c3 === "no") && review.verdict === "promote") {
|
|
310
|
-
failures.push("a criterion 1..3 judgment is no but verdict is promote");
|
|
311
|
-
}
|
|
312
|
-
if (c5 === "no" && review.verdict === "promote") {
|
|
313
|
-
failures.push("criterion 5 is no (duplicate of existing) but verdict is promote");
|
|
314
|
-
}
|
|
315
|
-
return { passed: failures.length === 0, failures };
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Convert a raw LLM item object into a typed PanelMemberReview. Returns null
|
|
319
|
-
* when the raw object is structurally malformed — callers treat this as a
|
|
320
|
-
* validator failure.
|
|
321
|
-
*/
|
|
322
|
-
function normalizeRawItem(raw, ctx) {
|
|
323
|
-
if (typeof raw.candidate_id !== "string")
|
|
324
|
-
return null;
|
|
325
|
-
const candidate = ctx.candidateById.get(raw.candidate_id);
|
|
326
|
-
if (!candidate)
|
|
327
|
-
return null;
|
|
328
|
-
const verdict = raw.verdict;
|
|
329
|
-
const rawCriteria = Array.isArray(raw.criteria) ? raw.criteria : [];
|
|
330
|
-
const criteria = [];
|
|
331
|
-
for (const r of rawCriteria) {
|
|
332
|
-
const rr = r;
|
|
333
|
-
const cnum = rr.criterion;
|
|
334
|
-
if (typeof cnum !== "number")
|
|
335
|
-
continue;
|
|
336
|
-
if (cnum < 1 || cnum > 5)
|
|
337
|
-
continue;
|
|
338
|
-
const judgment = rr.judgment;
|
|
339
|
-
if (judgment !== "yes" && judgment !== "no" && judgment !== "uncertain") {
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
criteria.push({
|
|
343
|
-
criterion: cnum,
|
|
344
|
-
judgment,
|
|
345
|
-
reasoning: typeof rr.reasoning === "string" ? rr.reasoning : "",
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
const review = {
|
|
349
|
-
member: ctx.member,
|
|
350
|
-
verdict,
|
|
351
|
-
criteria,
|
|
352
|
-
axis_tag_recommendation: raw.axis_tag_recommendation ??
|
|
353
|
-
"no_recommendation",
|
|
354
|
-
axis_tag_note: typeof raw.axis_tag_note === "string" ? raw.axis_tag_note : "",
|
|
355
|
-
reason: typeof raw.reason === "string" ? raw.reason : "",
|
|
356
|
-
llm_model_id: ctx.llm_model_id,
|
|
357
|
-
llm_prompt_hash: ctx.llm_prompt_hash,
|
|
358
|
-
};
|
|
359
|
-
if (raw.contradiction_resolution === "replace" ||
|
|
360
|
-
raw.contradiction_resolution === "defer" ||
|
|
361
|
-
raw.contradiction_resolution === "n/a") {
|
|
362
|
-
review.contradiction_resolution = raw.contradiction_resolution;
|
|
363
|
-
}
|
|
364
|
-
return { review, candidate };
|
|
365
|
-
}
|
|
366
|
-
export async function callPanelMember(config) {
|
|
367
|
-
const candidateById = new Map(config.candidates.map((c) => [candidateIdOf(c), c]));
|
|
368
|
-
const globalsByLine = new Map(config.globalItems.map((g) => [g.raw_line, g]));
|
|
369
|
-
const tryOnce = async (retryFeedback) => {
|
|
370
|
-
const promptCfg = {
|
|
371
|
-
member: config.member,
|
|
372
|
-
candidates: config.candidates,
|
|
373
|
-
globalItems: config.globalItems,
|
|
374
|
-
};
|
|
375
|
-
if (config.maxGlobalItems !== undefined) {
|
|
376
|
-
promptCfg.maxGlobalItems = config.maxGlobalItems;
|
|
377
|
-
}
|
|
378
|
-
if (retryFeedback !== undefined) {
|
|
379
|
-
promptCfg.retryFeedback = retryFeedback;
|
|
380
|
-
}
|
|
381
|
-
const prompt = buildPanelPrompt(promptCfg);
|
|
382
|
-
let llmText;
|
|
383
|
-
let modelId;
|
|
384
|
-
try {
|
|
385
|
-
const result = await callLlm(prompt.system_prompt, prompt.user_prompt, {
|
|
386
|
-
max_tokens: config.maxTokens ?? 4096,
|
|
387
|
-
...(config.modelId ? { model_id: config.modelId } : {}),
|
|
388
|
-
});
|
|
389
|
-
llmText = result.text;
|
|
390
|
-
modelId = result.model_id;
|
|
391
|
-
}
|
|
392
|
-
catch (error) {
|
|
393
|
-
return {
|
|
394
|
-
member: { ...config.member, reachable: false },
|
|
395
|
-
reviews: new Map(),
|
|
396
|
-
failures: [],
|
|
397
|
-
status: "unreachable",
|
|
398
|
-
unreachable_reason: error instanceof Error ? error.message : String(error),
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
let parsed;
|
|
402
|
-
try {
|
|
403
|
-
parsed = parsePanelJson(llmText);
|
|
404
|
-
}
|
|
405
|
-
catch (error) {
|
|
406
|
-
return {
|
|
407
|
-
member: config.member,
|
|
408
|
-
reviews: new Map(),
|
|
409
|
-
failures: [
|
|
410
|
-
`panel response not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
411
|
-
],
|
|
412
|
-
status: "contract_invalid",
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
const ctx = {
|
|
416
|
-
member: config.member,
|
|
417
|
-
candidateById,
|
|
418
|
-
globalsByLine,
|
|
419
|
-
llm_model_id: modelId,
|
|
420
|
-
llm_prompt_hash: prompt.prompt_hash,
|
|
421
|
-
};
|
|
422
|
-
const reviews = new Map();
|
|
423
|
-
const allFailures = [];
|
|
424
|
-
for (const raw of parsed.items) {
|
|
425
|
-
const normalized = normalizeRawItem(raw, ctx);
|
|
426
|
-
if (!normalized) {
|
|
427
|
-
allFailures.push(`item candidate_id=${String(raw.candidate_id)} could not be normalized (unknown id or malformed)`);
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
const validation = validatePanelMemberReview(normalized.review);
|
|
431
|
-
if (!validation.passed) {
|
|
432
|
-
const id = candidateIdOf(normalized.candidate);
|
|
433
|
-
for (const f of validation.failures) {
|
|
434
|
-
allFailures.push(`[${id}] ${f}`);
|
|
435
|
-
}
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
reviews.set(candidateIdOf(normalized.candidate), normalized.review);
|
|
439
|
-
}
|
|
440
|
-
// Missing candidates: LLM silently dropped some items
|
|
441
|
-
for (const c of config.candidates) {
|
|
442
|
-
const id = candidateIdOf(c);
|
|
443
|
-
if (!reviews.has(id) && !allFailures.some((f) => f.includes(id))) {
|
|
444
|
-
allFailures.push(`[${id}] no review returned for this candidate`);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
if (allFailures.length === 0) {
|
|
448
|
-
return {
|
|
449
|
-
member: config.member,
|
|
450
|
-
reviews,
|
|
451
|
-
failures: [],
|
|
452
|
-
status: "passed",
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
return {
|
|
456
|
-
member: config.member,
|
|
457
|
-
reviews,
|
|
458
|
-
failures: allFailures,
|
|
459
|
-
status: "contract_invalid",
|
|
460
|
-
};
|
|
461
|
-
};
|
|
462
|
-
const first = await tryOnce();
|
|
463
|
-
if (first.status === "passed" || first.status === "unreachable") {
|
|
464
|
-
return first;
|
|
465
|
-
}
|
|
466
|
-
const second = await tryOnce(first.failures);
|
|
467
|
-
if (second.status === "passed") {
|
|
468
|
-
return {
|
|
469
|
-
...second,
|
|
470
|
-
status: "retried_passed",
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
return second;
|
|
474
|
-
}
|
|
475
|
-
// ---------------------------------------------------------------------------
|
|
476
|
-
// Consensus aggregation — DD-12 hard gate included
|
|
477
|
-
// ---------------------------------------------------------------------------
|
|
478
|
-
function tallyVerdicts(reviews) {
|
|
479
|
-
const tally = { promote: 0, defer: 0, reject: 0 };
|
|
480
|
-
for (const r of reviews) {
|
|
481
|
-
tally[r.verdict] += 1;
|
|
482
|
-
}
|
|
483
|
-
return tally;
|
|
484
|
-
}
|
|
485
|
-
/**
|
|
486
|
-
* DD-12 hard gate + DD-7 consensus mapping.
|
|
487
|
-
*
|
|
488
|
-
* Rules:
|
|
489
|
-
* - valid_member_count < 2 → panel_minimum_unmet (hard gate)
|
|
490
|
-
* - 3-agent panel + all promote → promote_3_3
|
|
491
|
-
* - 2-agent degraded panel + both promote → promote_2_3
|
|
492
|
-
* (m-1 fix: previously the n===2 unanimous case fell into the
|
|
493
|
-
* promote_3_3 branch because tally.promote === n. The label was
|
|
494
|
-
* misleading because there were only 2 members, not 3.)
|
|
495
|
-
* - strict majority promote (>= 2) → promote_2_3
|
|
496
|
-
* - strict majority defer (>= 2) → defer_majority
|
|
497
|
-
* - strict majority reject (>= 2) → reject_majority
|
|
498
|
-
* - otherwise → split
|
|
499
|
-
*/
|
|
500
|
-
export function aggregateConsensus(validReviews) {
|
|
501
|
-
if (validReviews.length < 2)
|
|
502
|
-
return "panel_minimum_unmet";
|
|
503
|
-
const tally = tallyVerdicts(validReviews);
|
|
504
|
-
const n = validReviews.length;
|
|
505
|
-
// m-1 fix: 2-member degraded panel with both promoting maps to promote_2_3,
|
|
506
|
-
// not promote_3_3 (which implies a 3-member panel).
|
|
507
|
-
if (n === 2 && tally.promote === 2)
|
|
508
|
-
return "promote_2_3";
|
|
509
|
-
if (tally.promote === n)
|
|
510
|
-
return "promote_3_3";
|
|
511
|
-
if (tally.promote >= 2)
|
|
512
|
-
return "promote_2_3";
|
|
513
|
-
if (tally.defer >= 2)
|
|
514
|
-
return "defer_majority";
|
|
515
|
-
if (tally.reject >= 2)
|
|
516
|
-
return "reject_majority";
|
|
517
|
-
return "split";
|
|
518
|
-
}
|
|
519
|
-
function computeMinorityOpinion(consensus, validReviews) {
|
|
520
|
-
if (consensus !== "promote_2_3")
|
|
521
|
-
return undefined;
|
|
522
|
-
const dissenters = validReviews.filter((r) => r.verdict !== "promote");
|
|
523
|
-
if (dissenters.length === 0)
|
|
524
|
-
return undefined;
|
|
525
|
-
return dissenters
|
|
526
|
-
.map((r) => `${r.member.agent_id} (${r.verdict}): ${r.reason}`)
|
|
527
|
-
.join("; ");
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Compose a panel per candidate and run parallel LLM reviews.
|
|
531
|
-
*
|
|
532
|
-
* Design note: a single panel composition is shared across batched candidates
|
|
533
|
-
* from the same originator. For minimal Phase A correctness we compose per
|
|
534
|
-
* candidate — candidates from the same originator will redundantly construct
|
|
535
|
-
* the same panel member list, but each member's LLM call is per-candidate
|
|
536
|
-
* anyway, so there's no cost saving from merging here.
|
|
537
|
-
*
|
|
538
|
-
* Future optimization: group candidates by originator and issue one LLM call
|
|
539
|
-
* per (originator, axiology, auto_selected) tuple to save N×3 calls. The
|
|
540
|
-
* current shape correctly implements criterion 1~5 per candidate; batching
|
|
541
|
-
* is a cost improvement, not a correctness gap.
|
|
542
|
-
*/
|
|
543
|
-
export async function reviewPanel(config) {
|
|
544
|
-
const verdicts = [];
|
|
545
|
-
const degraded_states = [];
|
|
546
|
-
const isContradiction = (id) => config.contradictionCandidates?.has(id) ?? false;
|
|
547
|
-
for (const candidate of config.candidates) {
|
|
548
|
-
const candidateId = candidateIdOf(candidate);
|
|
549
|
-
const members = composePanel(candidate, config.ontoHome !== undefined ? { ontoHome: config.ontoHome } : {});
|
|
550
|
-
// One LLM call per member over a single-candidate batch. Batching across
|
|
551
|
-
// candidates per originator is a Step 13 optimization.
|
|
552
|
-
const memberResults = await Promise.all(members.map((member) => callPanelMember({
|
|
553
|
-
member,
|
|
554
|
-
candidates: [candidate],
|
|
555
|
-
globalItems: config.globalItems,
|
|
556
|
-
...(config.maxTokens !== undefined
|
|
557
|
-
? { maxTokens: config.maxTokens }
|
|
558
|
-
: {}),
|
|
559
|
-
...(config.modelId !== undefined ? { modelId: config.modelId } : {}),
|
|
560
|
-
...(config.maxGlobalItems !== undefined
|
|
561
|
-
? { maxGlobalItems: config.maxGlobalItems }
|
|
562
|
-
: {}),
|
|
563
|
-
})));
|
|
564
|
-
// Collect member_reviews that survived validation for this candidate.
|
|
565
|
-
const memberReviews = [];
|
|
566
|
-
const effectiveMembers = [];
|
|
567
|
-
for (const result of memberResults) {
|
|
568
|
-
if (result.status === "unreachable") {
|
|
569
|
-
effectiveMembers.push({
|
|
570
|
-
...result.member,
|
|
571
|
-
reachable: false,
|
|
572
|
-
unreachable_reason: result.unreachable_reason ?? "unknown",
|
|
573
|
-
});
|
|
574
|
-
degraded_states.push({
|
|
575
|
-
kind: "member_unreachable",
|
|
576
|
-
detail: `${result.member.agent_id}: ${result.unreachable_reason ?? "unknown"}`,
|
|
577
|
-
affected_candidates: [candidateId],
|
|
578
|
-
affected_agents: [result.member.agent_id],
|
|
579
|
-
occurred_at: new Date().toISOString(),
|
|
580
|
-
});
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
effectiveMembers.push(result.member);
|
|
584
|
-
if (result.status === "contract_invalid") {
|
|
585
|
-
degraded_states.push({
|
|
586
|
-
kind: "panel_contract_invalid",
|
|
587
|
-
detail: `${result.member.agent_id}: ${result.failures.join("; ")}`,
|
|
588
|
-
affected_candidates: [candidateId],
|
|
589
|
-
affected_agents: [result.member.agent_id],
|
|
590
|
-
occurred_at: new Date().toISOString(),
|
|
591
|
-
});
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
// passed or retried_passed
|
|
595
|
-
const review = result.reviews.get(candidateId);
|
|
596
|
-
if (!review) {
|
|
597
|
-
degraded_states.push({
|
|
598
|
-
kind: "panel_contract_invalid",
|
|
599
|
-
detail: `${result.member.agent_id}: validated call returned no review for candidate`,
|
|
600
|
-
affected_candidates: [candidateId],
|
|
601
|
-
affected_agents: [result.member.agent_id],
|
|
602
|
-
occurred_at: new Date().toISOString(),
|
|
603
|
-
});
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
memberReviews.push(review);
|
|
607
|
-
}
|
|
608
|
-
const consensus = aggregateConsensus(memberReviews);
|
|
609
|
-
if (consensus === "panel_minimum_unmet") {
|
|
610
|
-
degraded_states.push({
|
|
611
|
-
kind: "panel_minimum_unmet",
|
|
612
|
-
detail: `candidate ${candidateId}: valid_member_count=${memberReviews.length}`,
|
|
613
|
-
affected_candidates: [candidateId],
|
|
614
|
-
occurred_at: new Date().toISOString(),
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
const verdict = {
|
|
618
|
-
candidate_id: candidateId,
|
|
619
|
-
candidate,
|
|
620
|
-
panel_members: effectiveMembers,
|
|
621
|
-
member_reviews: memberReviews,
|
|
622
|
-
consensus,
|
|
623
|
-
is_contradiction: isContradiction(candidateId),
|
|
624
|
-
matched_existing_line: null,
|
|
625
|
-
};
|
|
626
|
-
const minority = computeMinorityOpinion(consensus, memberReviews);
|
|
627
|
-
if (minority !== undefined) {
|
|
628
|
-
verdict.minority_opinion = minority;
|
|
629
|
-
}
|
|
630
|
-
verdicts.push(verdict);
|
|
631
|
-
}
|
|
632
|
-
return { verdicts, degraded_states };
|
|
633
|
-
}
|
|
634
|
-
// ---------------------------------------------------------------------------
|
|
635
|
-
// Criterion 6 — cross-agent dedup (LLM-driven)
|
|
636
|
-
// ---------------------------------------------------------------------------
|
|
637
|
-
/**
|
|
638
|
-
* Cross-agent deduplication (criterion 6) — single-reviewer sequential path
|
|
639
|
-
* with bi-directional removal protection.
|
|
640
|
-
*
|
|
641
|
-
* Algorithm:
|
|
642
|
-
* 1. Pre-filter via Jaccard token overlap on significant content tokens.
|
|
643
|
-
* Cross-agent pairs (different agent_id) with similarity ≥ JACCARD_THRESHOLD
|
|
644
|
-
* become edges in a union-find structure.
|
|
645
|
-
* 2. Each resulting connected component with ≥ 2 distinct agents becomes a
|
|
646
|
-
* "shortlist" candidate for LLM confirmation.
|
|
647
|
-
* 3. The shortlist is capped at MAX_ITEMS_PER_SHORTLIST and the total number
|
|
648
|
-
* of shortlists at MAX_SHORTLISTS_PER_RUN to bound LLM cost.
|
|
649
|
-
* 4. Per shortlist, one LLM call applies the same-principle test with
|
|
650
|
-
* agent-specific framing removed (not domain terms). The model returns
|
|
651
|
-
* a structured JSON: primary owner + consolidated principle + cases.
|
|
652
|
-
* 5. Confirmed clusters become CrossAgentDedupCluster entries.
|
|
653
|
-
*
|
|
654
|
-
* Single-reviewer semantics (not 3-agent): promote.md §3 criterion 6 notes
|
|
655
|
-
* that parallel 3-agent review risks bi-directional deletion (agent A removes
|
|
656
|
-
* B while agent B removes A). The discovery path is intentionally one voice.
|
|
657
|
-
*
|
|
658
|
-
* Pre-filter rationale: an O(N²) naive LLM pass over candidates + globals is
|
|
659
|
-
* cost-prohibitive at production scale (117 candidates × 1000 globals).
|
|
660
|
-
* Jaccard is cheap, deterministic, and keeps the LLM load bounded.
|
|
661
|
-
*
|
|
662
|
-
* Failure model:
|
|
663
|
-
* - LLM unreachable or returns malformed JSON for a shortlist → that shortlist
|
|
664
|
-
* is dropped. Other shortlists proceed. (Recording degraded_states for
|
|
665
|
-
* dropped discovery shortlists is a follow-up — the current caller
|
|
666
|
-
* doesn't propagate them.)
|
|
667
|
-
*/
|
|
668
|
-
const CROSS_AGENT_DEDUP_SYSTEM_PROMPT = `You are detecting cross-agent principle duplication in a learning management system.
|
|
669
|
-
|
|
670
|
-
You will receive 2 or more learnings from DIFFERENT agents. Apply the same-principle test to decide whether they express the same underlying principle once agent-specific framing is removed:
|
|
671
|
-
|
|
672
|
-
(a) Remove agent-specific framing (e.g. "the axiology lens asks...", "structurally...") from both items.
|
|
673
|
-
(b) Do the remaining sentences prescribe the same action?
|
|
674
|
-
(c) Can you identify a situation where one applies but the other does not? If yes, they are different principles.
|
|
675
|
-
|
|
676
|
-
If they ARE the same principle:
|
|
677
|
-
- Pick a primary_owner_agent: the agent closest to the verification dimension of the principle.
|
|
678
|
-
Tiebreaker: the agent of the earliest-created learning (oldest source_date).
|
|
679
|
-
- Write a consolidated_principle statement that generalizes over the agents.
|
|
680
|
-
- Pick up to 3 representative_cases that maximize agent diversity.
|
|
681
|
-
- Compose a consolidated_line in the flat inline format:
|
|
682
|
-
"- [{type}] [{axis tags}] [{purpose type}] General principle statement. (Representative cases: agent-A에서 X; agent-B에서 Y; agent-C에서 Z) (source: consolidated from [sources])"
|
|
683
|
-
|
|
684
|
-
Output ONE JSON object:
|
|
685
|
-
{
|
|
686
|
-
"same_principle": true | false,
|
|
687
|
-
"primary_owner_agent": "<agent_id>" | null,
|
|
688
|
-
"primary_owner_reason": "<string>",
|
|
689
|
-
"consolidated_principle": "<string>",
|
|
690
|
-
"representative_cases": ["<case 1>", "<case 2>", "<case 3>"],
|
|
691
|
-
"consolidated_line": "<inline format line>"
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
NO markdown fences, JSON only.`;
|
|
695
|
-
const JACCARD_THRESHOLD = 0.3;
|
|
696
|
-
const MAX_SHORTLISTS_PER_RUN = 20;
|
|
697
|
-
const MAX_ITEMS_PER_SHORTLIST = 10;
|
|
698
|
-
const MIN_TOKEN_LENGTH = 4;
|
|
699
|
-
const MIN_CJK_TOKEN_LENGTH = 2;
|
|
700
|
-
const STOPWORDS = new Set([
|
|
701
|
-
"with",
|
|
702
|
-
"that",
|
|
703
|
-
"this",
|
|
704
|
-
"from",
|
|
705
|
-
"into",
|
|
706
|
-
"when",
|
|
707
|
-
"where",
|
|
708
|
-
"what",
|
|
709
|
-
"which",
|
|
710
|
-
"these",
|
|
711
|
-
"those",
|
|
712
|
-
"have",
|
|
713
|
-
"been",
|
|
714
|
-
"being",
|
|
715
|
-
"should",
|
|
716
|
-
"would",
|
|
717
|
-
"could",
|
|
718
|
-
"will",
|
|
719
|
-
"must",
|
|
720
|
-
"does",
|
|
721
|
-
"they",
|
|
722
|
-
"them",
|
|
723
|
-
"their",
|
|
724
|
-
"there",
|
|
725
|
-
"then",
|
|
726
|
-
"than",
|
|
727
|
-
"some",
|
|
728
|
-
"about",
|
|
729
|
-
"also",
|
|
730
|
-
"because",
|
|
731
|
-
"such",
|
|
732
|
-
"each",
|
|
733
|
-
"while",
|
|
734
|
-
"after",
|
|
735
|
-
"before",
|
|
736
|
-
]);
|
|
737
|
-
/**
|
|
738
|
-
* U4 fix: Unicode-aware tokenization. Previously the splitter was
|
|
739
|
-
* `[^a-z0-9]+`, which stripped every Korean (and other non-Latin)
|
|
740
|
-
* character. On a Korean-heavy corpus (like this repo's own learnings),
|
|
741
|
-
* that made criterion 6 effectively blind — no shortlist ever formed.
|
|
742
|
-
*
|
|
743
|
-
* New behavior:
|
|
744
|
-
* - Split on characters that are NOT Unicode letters or numbers
|
|
745
|
-
* (`\p{L}` / `\p{N}` with the `u` flag).
|
|
746
|
-
* - Latin tokens still require MIN_TOKEN_LENGTH (4) to avoid matching
|
|
747
|
-
* on short words like "with" or "that".
|
|
748
|
-
* - Korean tokens (CJK ideographs and Hangul) use a lower threshold
|
|
749
|
-
* (MIN_CJK_TOKEN_LENGTH = 2) because one-syllable Korean words carry
|
|
750
|
-
* content ("코드", "검증", etc.).
|
|
751
|
-
* - English stopwords still filtered.
|
|
752
|
-
*/
|
|
753
|
-
function significantTokens(content) {
|
|
754
|
-
const tokens = new Set();
|
|
755
|
-
// Match runs of Unicode letters/numbers rather than splitting on
|
|
756
|
-
// ASCII punctuation only.
|
|
757
|
-
const matches = content.toLowerCase().match(/[\p{L}\p{N}]+/gu);
|
|
758
|
-
if (!matches)
|
|
759
|
-
return tokens;
|
|
760
|
-
for (const word of matches) {
|
|
761
|
-
if (STOPWORDS.has(word))
|
|
762
|
-
continue;
|
|
763
|
-
if (isCjkWord(word)) {
|
|
764
|
-
if (word.length < MIN_CJK_TOKEN_LENGTH)
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
else if (word.length < MIN_TOKEN_LENGTH) {
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
tokens.add(word);
|
|
771
|
-
}
|
|
772
|
-
return tokens;
|
|
773
|
-
}
|
|
774
|
-
// Hangul syllables + jamo + CJK unified ideographs cover Korean content.
|
|
775
|
-
const CJK_RE = /[\u3040-\u30ff\u3130-\u318f\uac00-\ud7af\u4e00-\u9fff]/;
|
|
776
|
-
function isCjkWord(word) {
|
|
777
|
-
return CJK_RE.test(word);
|
|
778
|
-
}
|
|
779
|
-
function jaccard(a, b) {
|
|
780
|
-
if (a.size === 0 || b.size === 0)
|
|
781
|
-
return 0;
|
|
782
|
-
let inter = 0;
|
|
783
|
-
for (const t of a)
|
|
784
|
-
if (b.has(t))
|
|
785
|
-
inter += 1;
|
|
786
|
-
const union = a.size + b.size - inter;
|
|
787
|
-
return union === 0 ? 0 : inter / union;
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* Minimal union-find with path compression. Indices are the slot positions in
|
|
791
|
-
* the flattened item pool (candidates ++ globals).
|
|
792
|
-
*/
|
|
793
|
-
class UnionFind {
|
|
794
|
-
parent;
|
|
795
|
-
constructor(size) {
|
|
796
|
-
this.parent = Array.from({ length: size }, (_, i) => i);
|
|
797
|
-
}
|
|
798
|
-
find(x) {
|
|
799
|
-
let cur = x;
|
|
800
|
-
while (this.parent[cur] !== cur) {
|
|
801
|
-
this.parent[cur] = this.parent[this.parent[cur]];
|
|
802
|
-
cur = this.parent[cur];
|
|
803
|
-
}
|
|
804
|
-
return cur;
|
|
805
|
-
}
|
|
806
|
-
union(a, b) {
|
|
807
|
-
const ra = this.find(a);
|
|
808
|
-
const rb = this.find(b);
|
|
809
|
-
if (ra !== rb)
|
|
810
|
-
this.parent[ra] = rb;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
function buildShortlists(items) {
|
|
814
|
-
const empty = {
|
|
815
|
-
shortlists: [],
|
|
816
|
-
total_valid_groups: 0,
|
|
817
|
-
shortlists_truncated_count: 0,
|
|
818
|
-
members_truncated_total: 0,
|
|
819
|
-
shortlists_cap_dropped_count: 0,
|
|
820
|
-
};
|
|
821
|
-
if (items.length < 2)
|
|
822
|
-
return empty;
|
|
823
|
-
const tokens = items.map((it) => significantTokens(it.content));
|
|
824
|
-
const uf = new UnionFind(items.length);
|
|
825
|
-
for (let i = 0; i < items.length; i++) {
|
|
826
|
-
for (let j = i + 1; j < items.length; j++) {
|
|
827
|
-
if (items[i].agent_id === items[j].agent_id)
|
|
828
|
-
continue;
|
|
829
|
-
const sim = jaccard(tokens[i], tokens[j]);
|
|
830
|
-
if (sim >= JACCARD_THRESHOLD)
|
|
831
|
-
uf.union(i, j);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
// Group indices by root
|
|
835
|
-
const groups = new Map();
|
|
836
|
-
for (let i = 0; i < items.length; i++) {
|
|
837
|
-
const root = uf.find(i);
|
|
838
|
-
const bucket = groups.get(root);
|
|
839
|
-
if (bucket)
|
|
840
|
-
bucket.push(i);
|
|
841
|
-
else
|
|
842
|
-
groups.set(root, [i]);
|
|
843
|
-
}
|
|
844
|
-
// First pass: filter to VALID groups (≥2 items, ≥2 distinct agents) and
|
|
845
|
-
// record total valid group count so cap-drop tallies are accurate.
|
|
846
|
-
const sortedRoots = [...groups.keys()].sort((a, b) => a - b);
|
|
847
|
-
const validGroups = [];
|
|
848
|
-
for (const root of sortedRoots) {
|
|
849
|
-
const indices = groups.get(root);
|
|
850
|
-
if (indices.length < 2)
|
|
851
|
-
continue;
|
|
852
|
-
const agents = new Set(indices.map((idx) => items[idx].agent_id));
|
|
853
|
-
if (agents.size < 2)
|
|
854
|
-
continue;
|
|
855
|
-
validGroups.push(indices);
|
|
856
|
-
}
|
|
857
|
-
// Second pass: apply the per-shortlist size cap and the total shortlist
|
|
858
|
-
// count cap while recording bounded-loss metrics for C4.
|
|
859
|
-
const shortlists = [];
|
|
860
|
-
let shortlistsTruncatedCount = 0;
|
|
861
|
-
let membersTruncatedTotal = 0;
|
|
862
|
-
for (const indices of validGroups) {
|
|
863
|
-
if (shortlists.length >= MAX_SHORTLISTS_PER_RUN)
|
|
864
|
-
break;
|
|
865
|
-
let capped = indices;
|
|
866
|
-
if (indices.length > MAX_ITEMS_PER_SHORTLIST) {
|
|
867
|
-
capped = indices.slice(0, MAX_ITEMS_PER_SHORTLIST);
|
|
868
|
-
shortlistsTruncatedCount += 1;
|
|
869
|
-
membersTruncatedTotal += indices.length - MAX_ITEMS_PER_SHORTLIST;
|
|
870
|
-
}
|
|
871
|
-
shortlists.push(capped.map((idx) => items[idx]));
|
|
872
|
-
}
|
|
873
|
-
const shortlistsCapDroppedCount = Math.max(0, validGroups.length - shortlists.length);
|
|
874
|
-
return {
|
|
875
|
-
shortlists,
|
|
876
|
-
total_valid_groups: validGroups.length,
|
|
877
|
-
shortlists_truncated_count: shortlistsTruncatedCount,
|
|
878
|
-
members_truncated_total: membersTruncatedTotal,
|
|
879
|
-
shortlists_cap_dropped_count: shortlistsCapDroppedCount,
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
function buildCrossAgentDedupUserPrompt(items) {
|
|
883
|
-
const lines = [
|
|
884
|
-
"Learnings from different agents to compare:",
|
|
885
|
-
"",
|
|
886
|
-
];
|
|
887
|
-
items.forEach((item, i) => {
|
|
888
|
-
lines.push(`${i + 1}. agent_id=${item.agent_id}`, ` role=${item.role ?? "null"}`, ` tags=[${item.applicability_tags.join(" ")}]`, ` source=${item.source_project ?? "?"}/${item.source_domain ?? "?"}/${item.source_date ?? "?"}`, ` content: ${item.content}`, "");
|
|
889
|
-
});
|
|
890
|
-
lines.push("Apply the same-principle test and respond with the JSON object.");
|
|
891
|
-
return lines.join("\n");
|
|
892
|
-
}
|
|
893
|
-
async function llmConfirmCluster(items, modelId) {
|
|
894
|
-
let responseText;
|
|
895
|
-
try {
|
|
896
|
-
const result = await callLlm(CROSS_AGENT_DEDUP_SYSTEM_PROMPT, buildCrossAgentDedupUserPrompt(items), {
|
|
897
|
-
max_tokens: 1024,
|
|
898
|
-
...(modelId ? { model_id: modelId } : {}),
|
|
899
|
-
});
|
|
900
|
-
responseText = result.text;
|
|
901
|
-
}
|
|
902
|
-
catch (error) {
|
|
903
|
-
return {
|
|
904
|
-
ok: false,
|
|
905
|
-
failure: {
|
|
906
|
-
kind: "provider_error",
|
|
907
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
908
|
-
},
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
let parsed;
|
|
912
|
-
try {
|
|
913
|
-
let cleaned = responseText.trim();
|
|
914
|
-
if (cleaned.startsWith("```")) {
|
|
915
|
-
cleaned = cleaned.replace(/^```(?:json)?\s*/, "").replace(/```\s*$/, "");
|
|
916
|
-
}
|
|
917
|
-
parsed = JSON.parse(cleaned);
|
|
918
|
-
}
|
|
919
|
-
catch (error) {
|
|
920
|
-
return {
|
|
921
|
-
ok: false,
|
|
922
|
-
failure: {
|
|
923
|
-
kind: "malformed_json",
|
|
924
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
925
|
-
},
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
if (parsed.same_principle !== true) {
|
|
929
|
-
return { ok: false, failure: { kind: "same_principle_false" } };
|
|
930
|
-
}
|
|
931
|
-
if (typeof parsed.primary_owner_agent !== "string") {
|
|
932
|
-
return { ok: false, failure: { kind: "missing_field", field: "primary_owner_agent" } };
|
|
933
|
-
}
|
|
934
|
-
if (typeof parsed.consolidated_principle !== "string") {
|
|
935
|
-
return { ok: false, failure: { kind: "missing_field", field: "consolidated_principle" } };
|
|
936
|
-
}
|
|
937
|
-
if (typeof parsed.consolidated_line !== "string") {
|
|
938
|
-
return { ok: false, failure: { kind: "missing_field", field: "consolidated_line" } };
|
|
939
|
-
}
|
|
940
|
-
if (!Array.isArray(parsed.representative_cases)) {
|
|
941
|
-
return { ok: false, failure: { kind: "missing_field", field: "representative_cases" } };
|
|
942
|
-
}
|
|
943
|
-
// C2 fix: primary_owner_agent must be one of the shortlist members. The
|
|
944
|
-
// LLM can hallucinate an agent_id or pick an unrelated one; we fail closed
|
|
945
|
-
// so approval never routes to an off-shortlist file.
|
|
946
|
-
const shortlistAgents = new Set(items.map((it) => it.agent_id));
|
|
947
|
-
if (!shortlistAgents.has(parsed.primary_owner_agent)) {
|
|
948
|
-
return {
|
|
949
|
-
ok: false,
|
|
950
|
-
failure: {
|
|
951
|
-
kind: "primary_owner_not_in_shortlist",
|
|
952
|
-
declared: parsed.primary_owner_agent,
|
|
953
|
-
},
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
return {
|
|
957
|
-
ok: true,
|
|
958
|
-
verdict: {
|
|
959
|
-
primary_owner_agent: parsed.primary_owner_agent,
|
|
960
|
-
primary_owner_reason: typeof parsed.primary_owner_reason === "string"
|
|
961
|
-
? parsed.primary_owner_reason
|
|
962
|
-
: "",
|
|
963
|
-
consolidated_principle: parsed.consolidated_principle,
|
|
964
|
-
representative_cases: parsed.representative_cases.filter((c) => typeof c === "string"),
|
|
965
|
-
consolidated_line: parsed.consolidated_line,
|
|
966
|
-
},
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
/**
|
|
970
|
-
* Stable id derived from member identity so repeat runs against unchanged
|
|
971
|
-
* inputs emit the same cluster_id.
|
|
972
|
-
*
|
|
973
|
-
* Stability caveat (review CC2):
|
|
974
|
-
* cluster_id is derived from the SHORTLIST members, not the full valid
|
|
975
|
-
* group. The shortlist may have been truncated by MAX_ITEMS_PER_SHORTLIST
|
|
976
|
-
* or the corpus may have grown between runs. Both conditions change the
|
|
977
|
-
* hashed member set and therefore the cluster_id.
|
|
978
|
-
*
|
|
979
|
-
* This is the intentional trade-off: cluster_id is meant to identify
|
|
980
|
-
* "the cluster the LLM reviewed in THIS run," not "the canonical cluster
|
|
981
|
-
* for this principle across all runs." The applicator uses cluster_id
|
|
982
|
-
* for within-run apply-state idempotency (matching a cluster_id marker
|
|
983
|
-
* in the target file when re-applying the same report). It is NOT a
|
|
984
|
-
* durable operator-facing identity across independent runs; treat
|
|
985
|
-
* cluster_id as session-scoped and re-derive it from the report JSON
|
|
986
|
-
* when you need to match an existing apply.
|
|
987
|
-
*
|
|
988
|
-
* If a future consumer needs cross-run identity, derive it from the
|
|
989
|
-
* consolidated_principle text + primary_owner_agent instead, and keep
|
|
990
|
-
* cluster_id as the run-local id.
|
|
991
|
-
*/
|
|
992
|
-
function hashCluster(items) {
|
|
993
|
-
const canonical = items
|
|
994
|
-
.map((it) => `${it.agent_id}|${it.content}`)
|
|
995
|
-
.sort()
|
|
996
|
-
.join("\n");
|
|
997
|
-
return crypto.createHash("sha256").update(canonical).digest("hex").slice(0, 12);
|
|
998
|
-
}
|
|
999
|
-
function emptyMetrics(poolSize) {
|
|
1000
|
-
return {
|
|
1001
|
-
pool_size: poolSize,
|
|
1002
|
-
total_valid_groups: 0,
|
|
1003
|
-
shortlists_processed: 0,
|
|
1004
|
-
shortlists_cap_dropped_count: 0,
|
|
1005
|
-
shortlists_truncated_count: 0,
|
|
1006
|
-
members_truncated_total: 0,
|
|
1007
|
-
same_principle_rejected: 0,
|
|
1008
|
-
llm_failures: {
|
|
1009
|
-
provider_error: 0,
|
|
1010
|
-
malformed_json: 0,
|
|
1011
|
-
missing_field: 0,
|
|
1012
|
-
primary_owner_not_in_shortlist: 0,
|
|
1013
|
-
},
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
export async function discoverCrossAgentDedupClusters(candidates, globalItems, config = {}) {
|
|
1017
|
-
// Candidates and globals are folded into a single pool so cross-scope
|
|
1018
|
-
// duplicates surface along with cross-agent duplicates inside either pool.
|
|
1019
|
-
const pool = [...candidates, ...globalItems];
|
|
1020
|
-
const metrics = emptyMetrics(pool.length);
|
|
1021
|
-
const built = buildShortlists(pool);
|
|
1022
|
-
metrics.total_valid_groups = built.total_valid_groups;
|
|
1023
|
-
metrics.shortlists_cap_dropped_count = built.shortlists_cap_dropped_count;
|
|
1024
|
-
metrics.shortlists_truncated_count = built.shortlists_truncated_count;
|
|
1025
|
-
metrics.members_truncated_total = built.members_truncated_total;
|
|
1026
|
-
if (built.shortlists.length === 0) {
|
|
1027
|
-
return { clusters: [], metrics };
|
|
1028
|
-
}
|
|
1029
|
-
const clusters = [];
|
|
1030
|
-
for (const shortlist of built.shortlists) {
|
|
1031
|
-
metrics.shortlists_processed += 1;
|
|
1032
|
-
const outcome = await llmConfirmCluster(shortlist, config.modelId);
|
|
1033
|
-
if (!outcome.ok) {
|
|
1034
|
-
switch (outcome.failure.kind) {
|
|
1035
|
-
case "provider_error":
|
|
1036
|
-
metrics.llm_failures.provider_error += 1;
|
|
1037
|
-
break;
|
|
1038
|
-
case "malformed_json":
|
|
1039
|
-
metrics.llm_failures.malformed_json += 1;
|
|
1040
|
-
break;
|
|
1041
|
-
case "same_principle_false":
|
|
1042
|
-
// UF2: not an llm_failure — valid negative classification.
|
|
1043
|
-
metrics.same_principle_rejected += 1;
|
|
1044
|
-
break;
|
|
1045
|
-
case "missing_field":
|
|
1046
|
-
metrics.llm_failures.missing_field += 1;
|
|
1047
|
-
break;
|
|
1048
|
-
case "primary_owner_not_in_shortlist":
|
|
1049
|
-
metrics.llm_failures.primary_owner_not_in_shortlist += 1;
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
continue;
|
|
1053
|
-
}
|
|
1054
|
-
const verdict = outcome.verdict;
|
|
1055
|
-
// CG1 + SYN-C1: Select the specific primary MEMBER INDEX (not raw_line
|
|
1056
|
-
// or agent_id) among shortlist members sharing the LLM-chosen
|
|
1057
|
-
// primary_owner_agent. An index is the only unambiguous identity when
|
|
1058
|
-
// multiple shortlist members share identical content.
|
|
1059
|
-
const primaryIndex = pickPrimaryMemberIndex(shortlist, verdict.primary_owner_agent);
|
|
1060
|
-
clusters.push({
|
|
1061
|
-
cluster_id: hashCluster(shortlist),
|
|
1062
|
-
primary_owner_agent: verdict.primary_owner_agent,
|
|
1063
|
-
primary_owner_reason: verdict.primary_owner_reason,
|
|
1064
|
-
primary_member_index: primaryIndex,
|
|
1065
|
-
consolidated_principle: verdict.consolidated_principle,
|
|
1066
|
-
representative_cases: verdict.representative_cases,
|
|
1067
|
-
member_items: shortlist,
|
|
1068
|
-
consolidated_line: verdict.consolidated_line,
|
|
1069
|
-
user_approval_required: true,
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
return { clusters, metrics };
|
|
1073
|
-
}
|
|
1074
|
-
/**
|
|
1075
|
-
* Pick the specific shortlist MEMBER INDEX that acts as the primary owner.
|
|
1076
|
-
*
|
|
1077
|
-
* Precondition: the shortlist was LLM-confirmed AND the owner agent was
|
|
1078
|
-
* validated against shortlist membership (the C2 guard), so at least one
|
|
1079
|
-
* member with `primary_owner_agent` is guaranteed to exist.
|
|
1080
|
-
*
|
|
1081
|
-
* Selection rule (promote.md §3 criterion 6 tiebreaker: "먼저 생성된 학습"):
|
|
1082
|
-
* 1. Filter shortlist to members whose `agent_id === primaryOwnerAgent`.
|
|
1083
|
-
* 2. Among those, prefer the member with the EARLIEST `source_date` in
|
|
1084
|
-
* ISO-8601 lexicographic order.
|
|
1085
|
-
* 3. Tiebreakers beyond source_date:
|
|
1086
|
-
* a. Dated members ALWAYS outrank null-dated members. A dated entry
|
|
1087
|
-
* carries verifiable provenance; a null-dated entry is a legacy
|
|
1088
|
-
* line with unknown age. When a timestamped and an untimed member
|
|
1089
|
-
* share the primary_owner_agent, the timestamped one wins.
|
|
1090
|
-
* b. Within all-null-dated members, the original shortlist ordering
|
|
1091
|
-
* is preserved (stable sort on equal keys).
|
|
1092
|
-
* c. Within equal source_date members, the original shortlist
|
|
1093
|
-
* ordering is preserved.
|
|
1094
|
-
* 4. The FIRST entry after sort is the winner; its ORIGINAL index in the
|
|
1095
|
-
* unsorted shortlist is returned (so downstream apply filters by slot
|
|
1096
|
-
* identity, not by content).
|
|
1097
|
-
*
|
|
1098
|
-
* Contract note (SYN-D1): the "null dated sorts AFTER dated" rule is
|
|
1099
|
-
* deliberate — "earliest known provenance" is a stronger signal than
|
|
1100
|
-
* "appears first in shortlist." If every owner candidate is null-dated,
|
|
1101
|
-
* the winner is the first one encountered, which is also stable and
|
|
1102
|
-
* deterministic (shortlist order is already deterministic per
|
|
1103
|
-
* buildShortlists).
|
|
1104
|
-
*/
|
|
1105
|
-
function pickPrimaryMemberIndex(shortlist, primaryOwnerAgent) {
|
|
1106
|
-
// Collect (index, item) pairs for owner candidates so we can sort by
|
|
1107
|
-
// source_date while preserving original slot positions.
|
|
1108
|
-
const ownerPairs = [];
|
|
1109
|
-
for (let i = 0; i < shortlist.length; i++) {
|
|
1110
|
-
const item = shortlist[i];
|
|
1111
|
-
if (item.agent_id === primaryOwnerAgent) {
|
|
1112
|
-
ownerPairs.push({ index: i, item });
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
// Guaranteed non-empty by the C2 precondition.
|
|
1116
|
-
//
|
|
1117
|
-
// 4-Rec3: Explicit slot tiebreaker. Previously this relied on
|
|
1118
|
-
// Array.prototype.sort's stability (ECMAScript 2019+ / Node 12+) to
|
|
1119
|
-
// preserve first-seen ordering on equal sort keys. That implicit
|
|
1120
|
-
// dependency is documented-but-fragile — a code reader inspecting this
|
|
1121
|
-
// function shouldn't have to know the Node engine floor to predict
|
|
1122
|
-
// tie-break behavior. We now use original slot index as the explicit
|
|
1123
|
-
// last tiebreaker in the comparator, so the selection is deterministic
|
|
1124
|
-
// regardless of the runtime's sort stability guarantees.
|
|
1125
|
-
ownerPairs.sort((a, b) => {
|
|
1126
|
-
const aDate = a.item.source_date;
|
|
1127
|
-
const bDate = b.item.source_date;
|
|
1128
|
-
// Rule 1: dated BEFORE null-dated (stronger provenance wins).
|
|
1129
|
-
if (aDate === null && bDate === null) {
|
|
1130
|
-
// Rule 3 (tiebreaker): lower slot index wins — explicit, no
|
|
1131
|
-
// stability reliance.
|
|
1132
|
-
return a.index - b.index;
|
|
1133
|
-
}
|
|
1134
|
-
if (aDate === null)
|
|
1135
|
-
return 1;
|
|
1136
|
-
if (bDate === null)
|
|
1137
|
-
return -1;
|
|
1138
|
-
// Rule 2: both dated — ascending lexicographic (earliest first).
|
|
1139
|
-
const cmp = aDate.localeCompare(bDate);
|
|
1140
|
-
if (cmp !== 0)
|
|
1141
|
-
return cmp;
|
|
1142
|
-
// Rule 3 (tiebreaker): equal dates → lower slot index wins.
|
|
1143
|
-
return a.index - b.index;
|
|
1144
|
-
});
|
|
1145
|
-
return ownerPairs[0].index;
|
|
1146
|
-
}
|
|
1147
|
-
// Test-only exports for unit coverage. Production imports go through
|
|
1148
|
-
// discoverCrossAgentDedupClusters.
|
|
1149
|
-
export const __testExports = {
|
|
1150
|
-
significantTokens,
|
|
1151
|
-
jaccard,
|
|
1152
|
-
buildShortlists,
|
|
1153
|
-
pickPrimaryMemberIndex,
|
|
1154
|
-
JACCARD_THRESHOLD,
|
|
1155
|
-
MAX_SHORTLISTS_PER_RUN,
|
|
1156
|
-
MAX_ITEMS_PER_SHORTLIST,
|
|
1157
|
-
MIN_CJK_TOKEN_LENGTH,
|
|
1158
|
-
};
|