@event4u/agent-config 14.21.0 → 14.23.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +164 -187
- package/CREDITS.md +37 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/commands/pr/merge.md +47 -38
- package/dist/agent-src/commands/roadmap/next.md +8 -0
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +45 -13
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +4 -14
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +13 -13
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1813 -946
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +21 -3
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +10870 -7641
- package/dist/scripts/_lib/lean_projection_mode.js +108 -0
- package/dist/scripts/_lib/router_match.js +243 -0
- package/dist/scripts/_lib/rule_injection.js +216 -0
- package/dist/scripts/condense.js +52 -45
- package/dist/scripts/project_thin_rules.js +139 -19
- package/dist/server/schemas/settings.js +16 -3
- package/docs/MIGRATION.md +105 -1
- package/docs/contracts/settings-classes.md +40 -8
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
- package/llms.txt +1 -1
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/agent-settings.template.yml +47 -1
- package/src/config/continuity-surface.json +516 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/hook-token-budget.json +7 -6
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/config/routing-coverage-seed.json +9 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
- package/src/scripts/_cli/handoff_sessions.ts +14 -44
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_slot.ts +295 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/continuity_writer.ts +253 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +86 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/lean_projection_mode.ts +127 -0
- package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
- package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/session_eol.ts +44 -0
- package/src/scripts/_lib/session_index_trust.ts +346 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/audit_mcp_tools.ts +69 -25
- package/src/scripts/build_mcp_catalog.ts +24 -1
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_host_tree_parity.ts +291 -0
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_preamble_payload_budget.ts +136 -7
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_rule_projection_integrity.ts +102 -2
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/ci_settle.ts +205 -5
- package/src/scripts/code_graph/build.ts +217 -15
- package/src/scripts/code_graph/cli.ts +334 -15
- package/src/scripts/code_graph/detect.ts +38 -0
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +144 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +262 -6
- package/src/scripts/code_graph/types.ts +117 -3
- package/src/scripts/code_graph/validate.ts +26 -1
- package/src/scripts/code_graph/verbs.ts +590 -0
- package/src/scripts/condense.ts +54 -42
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +118 -1
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +87 -12
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/push_settle_hook.ts +3 -2
- package/src/scripts/hooks/rule_inject_hook.ts +108 -21
- package/src/scripts/hooks/session_eol_hook.ts +120 -2
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/hot_context_hook.ts +62 -7
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/lint_roadmap_blockers.ts +164 -1
- package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
- package/src/scripts/mcp_server/graph_tools.ts +294 -0
- package/src/scripts/mcp_server/path_util.ts +31 -0
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
- package/src/scripts/mcp_server/tools.ts +5 -69
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/model_rule_injection.ts +61 -4
- package/src/scripts/project_thin_rules.ts +149 -16
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/report_standing_payload_by_host.ts +650 -0
- package/src/scripts/schemas/agent-settings.schema.json +7 -1
- package/src/scripts/session_memory_index.ts +81 -8
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/server/schemas/settings.ts +23 -4
- package/src/vendor/grammars/README.md +86 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two per-run deliberation measurements, both computed from outputs the run has
|
|
3
|
+
* already paid for: a per-correction stage attribution, and the
|
|
4
|
+
* `zero_marginal_value_call_rate`.
|
|
5
|
+
*
|
|
6
|
+
* Pure and offline. No model call, no new similarity measure — every judgement
|
|
7
|
+
* below reuses `is_near_duplicate` from `debate_gates.ts`, which is the
|
|
8
|
+
* repository's existing answer to "did this reply add anything", and the
|
|
9
|
+
* objection vocabulary that module already publishes. Forking a second
|
|
10
|
+
* similarity notion here would be two answers to one question, and the second
|
|
11
|
+
* is the one nobody updates.
|
|
12
|
+
*
|
|
13
|
+
* WHAT A CORRECTION IS, AND WHAT IT IS NOT.
|
|
14
|
+
*
|
|
15
|
+
* A correction is an objection-bearing sentence: one that carries a marker from
|
|
16
|
+
* {@link CORRECTION_MARKERS} and therefore names something it is pushing back
|
|
17
|
+
* on. Its FIRST STAGE is the earliest stage whose text contains that sentence
|
|
18
|
+
* or a near-duplicate of it — so a point raised in round 1 and repeated in
|
|
19
|
+
* round 3 is attributed to round 1, which is the whole question 10.2 asks.
|
|
20
|
+
*
|
|
21
|
+
* It decides ATTRIBUTION, never USEFULNESS. Whether a correction improved the
|
|
22
|
+
* verdict is a benchmark question and the benchmark has not run; a mechanism
|
|
23
|
+
* here that claimed to score usefulness would be inventing the number the
|
|
24
|
+
* forbidden-claims list exists to prevent. {@link attributeCorrections} answers
|
|
25
|
+
* "where did this first appear", and the word "useful" in the step it serves is
|
|
26
|
+
* a filter a human applies to the output, not one this code applies for them.
|
|
27
|
+
*
|
|
28
|
+
* WHAT ZERO MARGINAL VALUE IS.
|
|
29
|
+
*
|
|
30
|
+
* A call has zero marginal value when its text near-duplicates something the
|
|
31
|
+
* run had ALREADY received when the call was made. Not "duplicates the member's
|
|
32
|
+
* own prior round" — a member restating a peer's answer verbatim adds exactly
|
|
33
|
+
* as little, and the per-member reading would score it as novel. So the
|
|
34
|
+
* comparison set is every successful call that preceded it in run order.
|
|
35
|
+
*
|
|
36
|
+
* The first successful call is never zero-marginal: nothing preceded it. That
|
|
37
|
+
* makes the denominator at least 1 whenever the run produced any usable call,
|
|
38
|
+
* which is why the rate is a number rather than a `null` on every real run —
|
|
39
|
+
* and a run that produced NO usable call gets `null` with a stated reason,
|
|
40
|
+
* because zero calls is not a rate of zero.
|
|
41
|
+
*
|
|
42
|
+
* HONEST SCOPE. This measures textual novelty against an 0.8 Jaccard bar. It
|
|
43
|
+
* does not measure whether a call changed the verdict, and no consumer may read
|
|
44
|
+
* it as decision-quality evidence. The threshold is the shipped
|
|
45
|
+
* `NOVELTY_DUP_THRESHOLD`, inherited rather than tuned here; nothing has been
|
|
46
|
+
* benchmarked against it.
|
|
47
|
+
*/
|
|
48
|
+
import type { CouncilResponse } from './clients.js';
|
|
49
|
+
import { is_near_duplicate, NOVELTY_DUP_THRESHOLD } from './debate_gates.js';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Objection markers. Deliberately the same vocabulary the shipped dissent
|
|
53
|
+
* counter uses, kept as its own constant because that one counts members and
|
|
54
|
+
* this one classifies sentences.
|
|
55
|
+
*/
|
|
56
|
+
export const CORRECTION_MARKERS =
|
|
57
|
+
/\b(disagree|disagrees|object|objection|reject|rejects|however|but|flaw|flawed|wrong|incorrect|counter|counter-position|dissent|contradict|contradicts|refute|refutes|mistaken|overstates|understates)\b/i;
|
|
58
|
+
|
|
59
|
+
/** One call the run made, in the order the run made it. */
|
|
60
|
+
export interface DeliberationCall {
|
|
61
|
+
/** Stage the call belongs to — a round label, a phase name, whatever the caller uses. */
|
|
62
|
+
readonly stage: string;
|
|
63
|
+
/** `provider/model`, or any stable member identity. */
|
|
64
|
+
readonly member: string;
|
|
65
|
+
/** The reply text. Empty or error replies are not scored. */
|
|
66
|
+
readonly text: string;
|
|
67
|
+
/** True when the transport reported a failure; such a call is not scored. */
|
|
68
|
+
readonly failed?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Where one correction first entered the deliberation. */
|
|
72
|
+
export interface CorrectionAttribution {
|
|
73
|
+
/** The correction sentence, trimmed and whitespace-collapsed. */
|
|
74
|
+
readonly correction: string;
|
|
75
|
+
/** Stage label of the earliest call carrying it. */
|
|
76
|
+
readonly firstStage: string;
|
|
77
|
+
/** Zero-based index of that call in run order. */
|
|
78
|
+
readonly firstCallIndex: number;
|
|
79
|
+
/** Member whose call first carried it. */
|
|
80
|
+
readonly firstMember: string;
|
|
81
|
+
/** How many later calls restated it. 0 means it was said once. */
|
|
82
|
+
readonly restatements: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* One deliberation stage and what it produced — the record shape
|
|
87
|
+
* `CouncilRouteRecord` carries, filled from a real run rather than by a test.
|
|
88
|
+
*/
|
|
89
|
+
export interface StageProduction {
|
|
90
|
+
readonly stage: string;
|
|
91
|
+
readonly produced: number;
|
|
92
|
+
readonly calls: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ZeroMarginalValueReading {
|
|
96
|
+
/**
|
|
97
|
+
* Zero-marginal calls over scored calls, or `null` when the run produced no
|
|
98
|
+
* scorable call at all. `null` is "not measurable here", never zero.
|
|
99
|
+
*/
|
|
100
|
+
readonly rate: number | null;
|
|
101
|
+
/** Why the rate is `null`. Empty string when it is not. */
|
|
102
|
+
readonly unavailableReason: string;
|
|
103
|
+
/** Every call the caller passed, failed and empty ones included. */
|
|
104
|
+
readonly callsTotal: number;
|
|
105
|
+
/** Calls that carried usable text — the denominator. */
|
|
106
|
+
readonly callsScored: number;
|
|
107
|
+
/** Calls that near-duplicated something already received — the numerator. */
|
|
108
|
+
readonly callsZeroMarginal: number;
|
|
109
|
+
/** The Jaccard bar used, published so the number can be re-derived. */
|
|
110
|
+
readonly threshold: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Decimals the published rate is rounded to, so a JSON round-trip is exact. */
|
|
114
|
+
export const RATE_PRECISION = 6;
|
|
115
|
+
|
|
116
|
+
function _round(value: number): number {
|
|
117
|
+
const f = 10 ** RATE_PRECISION;
|
|
118
|
+
return Math.round(value * f) / f;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _usable(c: DeliberationCall): boolean {
|
|
122
|
+
return c.failed !== true && c.text.trim().length > 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Split a reply into sentences. Crude on purpose: no NLP, no dependency. */
|
|
126
|
+
function _sentences(text: string): string[] {
|
|
127
|
+
return text
|
|
128
|
+
.split(/(?<=[.!?])\s+|\n+/)
|
|
129
|
+
.map((s) => s.replace(/\s+/g, ' ').trim())
|
|
130
|
+
.filter((s) => s.length > 0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Correction sentences in one reply.
|
|
135
|
+
*
|
|
136
|
+
* A minimum length is applied because a bare "But." carries a marker and no
|
|
137
|
+
* position; the bar is characters rather than a judgement so it stays
|
|
138
|
+
* predictable.
|
|
139
|
+
*/
|
|
140
|
+
export const MIN_CORRECTION_CHARS = 25;
|
|
141
|
+
|
|
142
|
+
export function correctionSentences(text: string): string[] {
|
|
143
|
+
return _sentences(text).filter(
|
|
144
|
+
(s) => s.length >= MIN_CORRECTION_CHARS && CORRECTION_MARKERS.test(s),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Attribute every correction to the earliest call that carried it.
|
|
150
|
+
*
|
|
151
|
+
* Calls are read in the order given, which must be run order — the attribution
|
|
152
|
+
* is only meaningful if "earlier in the list" means "earlier in the run".
|
|
153
|
+
*/
|
|
154
|
+
export function attributeCorrections(
|
|
155
|
+
calls: readonly DeliberationCall[],
|
|
156
|
+
threshold: number = NOVELTY_DUP_THRESHOLD,
|
|
157
|
+
): CorrectionAttribution[] {
|
|
158
|
+
const out: CorrectionAttribution[] = [];
|
|
159
|
+
const restatements: number[] = [];
|
|
160
|
+
|
|
161
|
+
calls.forEach((call, idx) => {
|
|
162
|
+
if (!_usable(call)) return;
|
|
163
|
+
for (const sentence of correctionSentences(call.text)) {
|
|
164
|
+
const seen = out.findIndex(
|
|
165
|
+
(a) =>
|
|
166
|
+
a.correction === sentence || is_near_duplicate(a.correction, sentence, threshold),
|
|
167
|
+
);
|
|
168
|
+
if (seen !== -1) {
|
|
169
|
+
restatements[seen] = (restatements[seen] ?? 0) + 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
restatements.push(0);
|
|
173
|
+
out.push({
|
|
174
|
+
correction: sentence,
|
|
175
|
+
firstStage: call.stage,
|
|
176
|
+
firstCallIndex: idx,
|
|
177
|
+
firstMember: call.member,
|
|
178
|
+
restatements: 0,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return out.map((a, i) => ({ ...a, restatements: restatements[i] ?? 0 }));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Per-stage production, derived from the attribution so the two can never
|
|
188
|
+
* disagree: `produced` counts the corrections FIRST attributed to that stage.
|
|
189
|
+
*/
|
|
190
|
+
export function stageProduction(
|
|
191
|
+
calls: readonly DeliberationCall[],
|
|
192
|
+
attributions: readonly CorrectionAttribution[],
|
|
193
|
+
): StageProduction[] {
|
|
194
|
+
const order: string[] = [];
|
|
195
|
+
const callCount = new Map<string, number>();
|
|
196
|
+
for (const c of calls) {
|
|
197
|
+
if (!callCount.has(c.stage)) {
|
|
198
|
+
order.push(c.stage);
|
|
199
|
+
callCount.set(c.stage, 0);
|
|
200
|
+
}
|
|
201
|
+
callCount.set(c.stage, (callCount.get(c.stage) ?? 0) + 1);
|
|
202
|
+
}
|
|
203
|
+
const produced = new Map<string, number>();
|
|
204
|
+
for (const a of attributions) {
|
|
205
|
+
produced.set(a.firstStage, (produced.get(a.firstStage) ?? 0) + 1);
|
|
206
|
+
}
|
|
207
|
+
return order.map((stage) => ({
|
|
208
|
+
stage,
|
|
209
|
+
produced: produced.get(stage) ?? 0,
|
|
210
|
+
calls: callCount.get(stage) ?? 0,
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The `zero_marginal_value_call_rate` for one run.
|
|
216
|
+
*
|
|
217
|
+
* Every scored call is compared against every scored call that preceded it. The
|
|
218
|
+
* comparison is O(n squared) in the number of calls, which is fine: a council
|
|
219
|
+
* run is single digits of calls per round.
|
|
220
|
+
*/
|
|
221
|
+
export function zeroMarginalValueRate(
|
|
222
|
+
calls: readonly DeliberationCall[],
|
|
223
|
+
threshold: number = NOVELTY_DUP_THRESHOLD,
|
|
224
|
+
): ZeroMarginalValueReading {
|
|
225
|
+
const received: string[] = [];
|
|
226
|
+
let zero = 0;
|
|
227
|
+
|
|
228
|
+
for (const call of calls) {
|
|
229
|
+
if (!_usable(call)) continue;
|
|
230
|
+
const dup = received.some((prior) => is_near_duplicate(prior, call.text, threshold));
|
|
231
|
+
if (dup) zero += 1;
|
|
232
|
+
received.push(call.text);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (received.length === 0) {
|
|
236
|
+
return {
|
|
237
|
+
rate: null,
|
|
238
|
+
unavailableReason:
|
|
239
|
+
'no scorable call — every call failed or returned empty text, and zero calls is not a rate of zero',
|
|
240
|
+
callsTotal: calls.length,
|
|
241
|
+
callsScored: 0,
|
|
242
|
+
callsZeroMarginal: 0,
|
|
243
|
+
threshold,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
rate: _round(zero / received.length),
|
|
249
|
+
unavailableReason: '',
|
|
250
|
+
callsTotal: calls.length,
|
|
251
|
+
callsZeroMarginal: zero,
|
|
252
|
+
callsScored: received.length,
|
|
253
|
+
threshold,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Everything this module emits for one run, ready to serialise. */
|
|
258
|
+
export interface DeliberationMetrics {
|
|
259
|
+
readonly zero_marginal_value_call_rate: number | null;
|
|
260
|
+
readonly zero_marginal_value_unavailable_reason: string;
|
|
261
|
+
readonly calls_total: number;
|
|
262
|
+
readonly calls_scored: number;
|
|
263
|
+
readonly calls_zero_marginal: number;
|
|
264
|
+
readonly novelty_threshold: number;
|
|
265
|
+
readonly stage_outputs: readonly StageProduction[];
|
|
266
|
+
readonly correction_attributions: readonly CorrectionAttribution[];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Compute both measurements in one pass over the run's calls. */
|
|
270
|
+
export function measureDeliberation(
|
|
271
|
+
calls: readonly DeliberationCall[],
|
|
272
|
+
threshold: number = NOVELTY_DUP_THRESHOLD,
|
|
273
|
+
): DeliberationMetrics {
|
|
274
|
+
const attributions = attributeCorrections(calls, threshold);
|
|
275
|
+
const zmv = zeroMarginalValueRate(calls, threshold);
|
|
276
|
+
return {
|
|
277
|
+
zero_marginal_value_call_rate: zmv.rate,
|
|
278
|
+
zero_marginal_value_unavailable_reason: zmv.unavailableReason,
|
|
279
|
+
calls_total: zmv.callsTotal,
|
|
280
|
+
calls_scored: zmv.callsScored,
|
|
281
|
+
calls_zero_marginal: zmv.callsZeroMarginal,
|
|
282
|
+
novelty_threshold: zmv.threshold,
|
|
283
|
+
stage_outputs: stageProduction(calls, attributions),
|
|
284
|
+
correction_attributions: attributions,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The parts of one council run that made provider calls, in the order the run
|
|
290
|
+
* made them.
|
|
291
|
+
*
|
|
292
|
+
* Assembled here rather than at the call site because "which calls did this run
|
|
293
|
+
* make, and at which stage" is this module's own subject, and the CLI had the
|
|
294
|
+
* list only as a flat array with the stage boundaries implicit in the order.
|
|
295
|
+
* Both readings come out of one walk, so the billing total and the attribution
|
|
296
|
+
* can never be computed over different sets.
|
|
297
|
+
*/
|
|
298
|
+
export interface RunCallParts {
|
|
299
|
+
/** The deliberation responses `consult()` returned. */
|
|
300
|
+
readonly deliberation: readonly CouncilResponse[];
|
|
301
|
+
readonly peerReview?: readonly CouncilResponse[] | null;
|
|
302
|
+
readonly consensusExtraction?: readonly CouncilResponse[] | null;
|
|
303
|
+
readonly consensusScoring?: readonly CouncilResponse[] | null;
|
|
304
|
+
readonly chairman?: CouncilResponse | null;
|
|
305
|
+
readonly stanceRepairs?: readonly CouncilResponse[] | null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The six stage labels, in run order. */
|
|
309
|
+
export const RUN_STAGES = Object.freeze([
|
|
310
|
+
'deliberation',
|
|
311
|
+
'peer-review',
|
|
312
|
+
'consensus-extraction',
|
|
313
|
+
'consensus-scoring',
|
|
314
|
+
'chairman',
|
|
315
|
+
'stance-repair',
|
|
316
|
+
] as const);
|
|
317
|
+
|
|
318
|
+
export interface RunCalls {
|
|
319
|
+
/** Stage-labelled calls, for {@link measureDeliberation}. */
|
|
320
|
+
readonly calls: readonly DeliberationCall[];
|
|
321
|
+
/** The same calls as raw responses, in the same order, for cost aggregation. */
|
|
322
|
+
readonly responses: CouncilResponse[];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function _asCall(stage: string, r: CouncilResponse): DeliberationCall {
|
|
326
|
+
return {
|
|
327
|
+
stage,
|
|
328
|
+
member: `${r.provider}/${r.model}`,
|
|
329
|
+
text: r.text,
|
|
330
|
+
failed: r.error !== null && r.error !== undefined,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Walk one run's parts into stage-labelled calls plus the flat response list. */
|
|
335
|
+
export function runCallsFrom(parts: RunCallParts): RunCalls {
|
|
336
|
+
const groups: ReadonlyArray<readonly [string, readonly CouncilResponse[]]> = [
|
|
337
|
+
['deliberation', parts.deliberation],
|
|
338
|
+
['peer-review', parts.peerReview ?? []],
|
|
339
|
+
['consensus-extraction', parts.consensusExtraction ?? []],
|
|
340
|
+
['consensus-scoring', parts.consensusScoring ?? []],
|
|
341
|
+
['chairman', parts.chairman === null || parts.chairman === undefined ? [] : [parts.chairman]],
|
|
342
|
+
['stance-repair', parts.stanceRepairs ?? []],
|
|
343
|
+
];
|
|
344
|
+
const calls: DeliberationCall[] = [];
|
|
345
|
+
const responses: CouncilResponse[] = [];
|
|
346
|
+
for (const [stage, group] of groups) {
|
|
347
|
+
for (const r of group) {
|
|
348
|
+
calls.push(_asCall(stage, r));
|
|
349
|
+
responses.push(r);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { calls, responses };
|
|
353
|
+
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { wrapUntrustedBlocks } from '../_lib/untrusted_content.js';
|
|
26
26
|
|
|
27
27
|
import type { ProjectContext } from './project_context.js';
|
|
28
|
+
import { RESOLVING_EVIDENCE_SECTION } from './synthesis_disagreement.js';
|
|
28
29
|
|
|
29
30
|
// Python: NEUTRALITY_PREAMBLE = """\...""".strip()
|
|
30
31
|
export const NEUTRALITY_PREAMBLE = `You are an independent reviewer. You have NOT seen any prior reasoning,
|
|
@@ -302,6 +303,8 @@ read it directly from a response.
|
|
|
302
303
|
A single sentence: which course the host agent should advise the
|
|
303
304
|
user to take, grounded in the strongest converged point.
|
|
304
305
|
|
|
306
|
+
${RESOLVING_EVIDENCE_SECTION}
|
|
307
|
+
|
|
305
308
|
### Kill criteria
|
|
306
309
|
Observable conditions that would falsify this recommendation without
|
|
307
310
|
re-convening the council. Each entry names a threshold or a specific
|
|
@@ -329,6 +332,8 @@ and the host agent confirms are load-bearing. Maximum five.
|
|
|
329
332
|
APPROVE / REQUEST_CHANGES / REJECT and a single sentence justifying
|
|
330
333
|
the verdict, anchored on the strongest consensus or must-fix line.
|
|
331
334
|
|
|
335
|
+
${RESOLVING_EVIDENCE_SECTION}
|
|
336
|
+
|
|
332
337
|
### Kill criteria
|
|
333
338
|
Observable conditions that would falsify this verdict without re-convening
|
|
334
339
|
the council (a failing check, a missing test, a regression signal). Each
|
|
@@ -363,6 +368,8 @@ separate certainty word for that; \`evidence:\` measures how well a
|
|
|
363
368
|
finding is supported, and being alone in raising it is a different
|
|
364
369
|
property the section already carries.
|
|
365
370
|
|
|
371
|
+
${RESOLVING_EVIDENCE_SECTION}
|
|
372
|
+
|
|
366
373
|
### Kill criteria
|
|
367
374
|
Observable conditions that would falsify the analysis's leading finding
|
|
368
375
|
without re-convening the council. Each entry names a threshold or event.
|
|
@@ -381,6 +388,8 @@ export const CREATIVE_SYNTHESIS = `Write a free-form convergence / divergence
|
|
|
381
388
|
synthesis of the council in prose — no fixed section shape for the body.
|
|
382
389
|
Then close with exactly these two sections:
|
|
383
390
|
|
|
391
|
+
${RESOLVING_EVIDENCE_SECTION}
|
|
392
|
+
|
|
384
393
|
### Kill criteria
|
|
385
394
|
Observable conditions that would falsify the direction you converged on,
|
|
386
395
|
without re-convening the council. Each entry names a threshold or event.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does a completed synthesis keep the disagreement it started with?
|
|
3
|
+
*
|
|
4
|
+
* Three elements have to survive into the final text, and a synthesis that
|
|
5
|
+
* drops any of them has resolved the disagreement by not reporting it:
|
|
6
|
+
*
|
|
7
|
+
* 1. the unresolved disagreement itself — both sides, named;
|
|
8
|
+
* 2. the strongest evidence the minority side actually offered;
|
|
9
|
+
* 3. the observation that would settle it.
|
|
10
|
+
*
|
|
11
|
+
* Element 1 was already asked for by every lens template (`Clashes`,
|
|
12
|
+
* `Conflicts`, `Outliers`, or the mandated divergence prose). Element 2 was
|
|
13
|
+
* asked for by one. Element 3 was asked for by none, which is the gap
|
|
14
|
+
* {@link RESOLVING_EVIDENCE_SECTION} closes.
|
|
15
|
+
*
|
|
16
|
+
* WHY A CHECKER AND NOT ONLY A TEMPLATE CLAUSE. A template is an instruction to
|
|
17
|
+
* the host agent, and an instruction nobody can check is a hope. Both council
|
|
18
|
+
* seats that reviewed this refused a prose-only build in as many words. So the
|
|
19
|
+
* template carries the ask and this module makes the answer decidable: given a
|
|
20
|
+
* completed synthesis, {@link auditSynthesisElements} reports which of the three
|
|
21
|
+
* elements are present, by structure, without deciding whether their content is
|
|
22
|
+
* any good.
|
|
23
|
+
*
|
|
24
|
+
* WHAT IT DECIDES, AND WHAT IT DOES NOT. Presence and shape. A synthesis that
|
|
25
|
+
* names a clash, cites a minority exhibit and proposes a resolving measurement
|
|
26
|
+
* PASSES even when the measurement is a bad one — grading that needs the
|
|
27
|
+
* benchmark, and the benchmark has not run. This is the same honest scope the
|
|
28
|
+
* sibling minority-retention audit states for itself, and for the same reason:
|
|
29
|
+
* an audit that claimed to grade quality would be inventing the number.
|
|
30
|
+
*
|
|
31
|
+
* REPORT, NEVER THROW. Not every council pass produces dissent, and a synthesis
|
|
32
|
+
* of a unanimous pass has no disagreement to retain. So the audit reports
|
|
33
|
+
* `dissentPresent: false` and grades nothing, rather than failing a synthesis
|
|
34
|
+
* for the absence of a thing that did not happen. A caller with independent
|
|
35
|
+
* knowledge that the pass dissented passes {@link AuditOptions.dissentPresent}
|
|
36
|
+
* and gets the full grading regardless of what the text looks like.
|
|
37
|
+
*
|
|
38
|
+
* Pure, offline, no model call.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** The three elements a dissenting synthesis has to carry. */
|
|
42
|
+
export type SynthesisElement =
|
|
43
|
+
| 'unresolved-disagreement'
|
|
44
|
+
| 'minority-evidence'
|
|
45
|
+
| 'resolving-evidence';
|
|
46
|
+
|
|
47
|
+
export const SYNTHESIS_ELEMENTS: readonly SynthesisElement[] = Object.freeze([
|
|
48
|
+
'unresolved-disagreement',
|
|
49
|
+
'minority-evidence',
|
|
50
|
+
'resolving-evidence',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The section every lens template now closes its disagreement reporting with.
|
|
55
|
+
*
|
|
56
|
+
* One section rather than two because elements 2 and 3 are both per-clash
|
|
57
|
+
* follow-ups: splitting them would let a synthesis answer one clash's evidence
|
|
58
|
+
* and another clash's resolution and still look complete.
|
|
59
|
+
*/
|
|
60
|
+
export const RESOLVING_EVIDENCE_SECTION = `### Resolving evidence
|
|
61
|
+
One line per unresolved disagreement above, in this shape:
|
|
62
|
+
\`<clash> — strongest minority evidence: <what the losing side actually
|
|
63
|
+
showed> · resolved by: <the observation that would settle it>\`.
|
|
64
|
+
Name a measurement, an artefact or a run — never a further opinion. Write
|
|
65
|
+
\`no resolving evidence identified — <why>\` when nothing observable would
|
|
66
|
+
settle it: an unanswerable disagreement is a finding, and silence is not.`;
|
|
67
|
+
|
|
68
|
+
/** Headings under which a lens reports an unresolved disagreement. */
|
|
69
|
+
const DISAGREEMENT_HEADINGS = ['Clashes', 'Conflicts', 'Outliers', 'Divergence'];
|
|
70
|
+
|
|
71
|
+
/** The heading element 3 lives under. */
|
|
72
|
+
export const RESOLVING_EVIDENCE_HEADING = 'Resolving evidence';
|
|
73
|
+
|
|
74
|
+
/** In-body markers the resolving-evidence section is asked to emit. */
|
|
75
|
+
const MINORITY_EVIDENCE_MARKER = /strongest minority evidence\s*:/i;
|
|
76
|
+
const RESOLVED_BY_MARKER = /resolved by\s*:/i;
|
|
77
|
+
const NO_RESOLVING_EVIDENCE_MARKER = /no resolving evidence identified\s*[—:-]/i;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Text markers that say a pass dissented, used only when the caller does not
|
|
81
|
+
* tell us. Deliberately narrow: a heading with a non-empty body, or an explicit
|
|
82
|
+
* split verdict. A loose match here would grade unanimous syntheses.
|
|
83
|
+
*/
|
|
84
|
+
const SPLIT_VERDICT_MARKER = /^\s*\*{0,2}verdict\*{0,2}\s*:\s*split\b/im;
|
|
85
|
+
|
|
86
|
+
export interface ElementFinding {
|
|
87
|
+
readonly element: SynthesisElement;
|
|
88
|
+
readonly present: boolean;
|
|
89
|
+
/** The heading or marker that satisfied it, or `null`. */
|
|
90
|
+
readonly satisfiedBy: string | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface SynthesisElementAudit {
|
|
94
|
+
/** Whether the pass had a disagreement to retain at all. */
|
|
95
|
+
readonly dissentPresent: boolean;
|
|
96
|
+
/** How `dissentPresent` was decided — `caller` or `text`. */
|
|
97
|
+
readonly dissentSource: 'caller' | 'text';
|
|
98
|
+
readonly elements: readonly ElementFinding[];
|
|
99
|
+
readonly missing: readonly SynthesisElement[];
|
|
100
|
+
/** True when a dissenting pass rendered all three. Always true with no dissent. */
|
|
101
|
+
readonly complete: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface AuditOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Override the text-derived dissent decision. Pass `true` when the run's own
|
|
107
|
+
* tally recorded a split; pass `false` to grade nothing.
|
|
108
|
+
*/
|
|
109
|
+
readonly dissentPresent?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _headingBody(text: string, heading: string): string | null {
|
|
113
|
+
const lines = text.split('\n');
|
|
114
|
+
const headingRe = new RegExp(`^#{2,4}\\s+${heading}\\s*$`, 'i');
|
|
115
|
+
const idx = lines.findIndex((l) => headingRe.test(l));
|
|
116
|
+
if (idx === -1) return null;
|
|
117
|
+
let body = '';
|
|
118
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
119
|
+
if (/^#{2,4}\s/.test(lines[i] as string)) break;
|
|
120
|
+
body += (lines[i] as string) + '\n';
|
|
121
|
+
}
|
|
122
|
+
return body;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A heading with a non-empty, non-placeholder body. */
|
|
126
|
+
function _nonEmptySection(text: string, heading: string): boolean {
|
|
127
|
+
const body = _headingBody(text, heading);
|
|
128
|
+
if (body === null) return false;
|
|
129
|
+
const stripped = body.replace(/^\s*[*_]*(none|n\/a|nothing)[.*_\s]*$/gim, '').trim();
|
|
130
|
+
return stripped.length > 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Audit a COMPLETED synthesis — the host- or chairman-authored text. */
|
|
134
|
+
export function auditSynthesisElements(
|
|
135
|
+
text: string,
|
|
136
|
+
opts: AuditOptions = {},
|
|
137
|
+
): SynthesisElementAudit {
|
|
138
|
+
const disagreementHeading =
|
|
139
|
+
DISAGREEMENT_HEADINGS.find((h) => _nonEmptySection(text, h)) ?? null;
|
|
140
|
+
|
|
141
|
+
const dissentSource: 'caller' | 'text' = opts.dissentPresent === undefined ? 'text' : 'caller';
|
|
142
|
+
const dissentPresent =
|
|
143
|
+
opts.dissentPresent ?? (disagreementHeading !== null || SPLIT_VERDICT_MARKER.test(text));
|
|
144
|
+
|
|
145
|
+
const resolvingBody = _headingBody(text, RESOLVING_EVIDENCE_HEADING) ?? '';
|
|
146
|
+
const declaredNone = NO_RESOLVING_EVIDENCE_MARKER.test(resolvingBody);
|
|
147
|
+
|
|
148
|
+
const elements: ElementFinding[] = [
|
|
149
|
+
{
|
|
150
|
+
element: 'unresolved-disagreement',
|
|
151
|
+
present: disagreementHeading !== null,
|
|
152
|
+
satisfiedBy: disagreementHeading === null ? null : `### ${disagreementHeading}`,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
element: 'minority-evidence',
|
|
156
|
+
present: MINORITY_EVIDENCE_MARKER.test(resolvingBody) || _nonEmptySection(text, 'Outliers'),
|
|
157
|
+
satisfiedBy: MINORITY_EVIDENCE_MARKER.test(resolvingBody)
|
|
158
|
+
? 'strongest minority evidence:'
|
|
159
|
+
: _nonEmptySection(text, 'Outliers')
|
|
160
|
+
? '### Outliers'
|
|
161
|
+
: null,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
element: 'resolving-evidence',
|
|
165
|
+
present: RESOLVED_BY_MARKER.test(resolvingBody) || declaredNone,
|
|
166
|
+
satisfiedBy: RESOLVED_BY_MARKER.test(resolvingBody)
|
|
167
|
+
? 'resolved by:'
|
|
168
|
+
: declaredNone
|
|
169
|
+
? 'no resolving evidence identified'
|
|
170
|
+
: null,
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const missing = dissentPresent
|
|
175
|
+
? elements.filter((e) => !e.present).map((e) => e.element)
|
|
176
|
+
: [];
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
dissentPresent,
|
|
180
|
+
dissentSource,
|
|
181
|
+
elements,
|
|
182
|
+
missing,
|
|
183
|
+
complete: missing.length === 0,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Human-readable audit text. */
|
|
188
|
+
export function renderSynthesisElementAudit(a: SynthesisElementAudit): string {
|
|
189
|
+
if (!a.dissentPresent) {
|
|
190
|
+
return (
|
|
191
|
+
'synthesis element audit: SKIPPED — no unresolved disagreement in this pass ' +
|
|
192
|
+
`(decided from ${a.dissentSource}); there is nothing to retain\n`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const rows = a.elements
|
|
196
|
+
.map(
|
|
197
|
+
(e) =>
|
|
198
|
+
` - ${e.element}: ${e.present ? 'present' : 'MISSING'}` +
|
|
199
|
+
(e.satisfiedBy === null ? '' : ` [${e.satisfiedBy}]`),
|
|
200
|
+
)
|
|
201
|
+
.join('\n');
|
|
202
|
+
const verdict = a.complete
|
|
203
|
+
? 'synthesis retains all three elements of the unresolved disagreement'
|
|
204
|
+
: `synthesis DROPS ${a.missing.length} of 3 elements: ${a.missing.join(', ')}`;
|
|
205
|
+
return `${verdict}\n${rows}\n`;
|
|
206
|
+
}
|