@event4u/agent-config 8.3.0 → 8.4.1
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/.claude-plugin/marketplace.json +4 -426
- package/.claude-plugin/skills/install-agent-config/SKILL.md +32 -0
- package/CHANGELOG.md +25 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/docs/contracts/surface-matrix.md +103 -0
- package/package.json +4 -1
- package/src/agent-src/README.md +64 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
- package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
- package/src/agent-src/templates/AGENTS.md +34 -0
- package/src/agent-src/templates/scripts/README.md +195 -0
- package/src/agent-src/templates/scripts/check_memory.ts +662 -0
- package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
- package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
- package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
- package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
- package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
- package/src/agent-src/templates/scripts/memory_report.ts +868 -0
- package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
- package/src/agent-src/templates/scripts/memory_status.ts +215 -0
- package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
- package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
- package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
- package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
- package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
- package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
- package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
- package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
- package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
- package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
- package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
- package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
- package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
- package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
- package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
- package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
- package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
- package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
- package/src/config/surface-matrix.yml +213 -0
- package/src/install/atomic.ts +111 -0
- package/src/install/conflict.ts +226 -0
- package/src/install/detect.ts +280 -0
- package/src/install/paths.ts +90 -0
- package/src/install/plan.ts +205 -0
- package/src/install/preflight.ts +210 -0
- package/src/install/selectedTools.ts +52 -0
- package/src/install/toolDetection.ts +120 -0
- package/src/install/txlog.ts +161 -0
- package/src/install/types.ts +175 -0
- package/src/install/wizard-plan.ts +216 -0
- package/src/scripts/_cli/cmd_converge.ts +399 -0
- package/src/scripts/_cli/cmd_doctor.ts +94 -0
- package/src/scripts/_cli/cmd_upgrade.ts +40 -16
- package/src/scripts/_dispatch.bash +13 -0
- package/src/scripts/condense.ts +67 -36
- package/src/scripts/hook_manifest.yaml +16 -7
- package/src/scripts/lint_marketplace.ts +54 -56
- package/src/scripts/lint_surface_matrix.ts +200 -0
- package/src/scripts/prepack-check.mjs +108 -5
- package/src/scripts/surface_probe_hook.ts +231 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confidence scoring for prompt-driven execution (R2 Phase 3 Step 2).
|
|
3
|
+
*
|
|
4
|
+
* TypeScript twin of `work_engine/scoring/confidence.py` (ADR-200 py2ts
|
|
5
|
+
* Phase 1 — work_engine scoring subpackage). Public API names stay snake_case
|
|
6
|
+
* to mirror the Python module 1:1 (per ADR-200 — Python style is part of the
|
|
7
|
+
* contract).
|
|
8
|
+
*
|
|
9
|
+
* The scorer judges whether a reconstructed prompt is good enough for the
|
|
10
|
+
* `work_engine` to proceed silently, halt for confirmation, or refuse to
|
|
11
|
+
* plan. It is heuristic-only — no LLM calls — so the same prompt produces
|
|
12
|
+
* the same score across replays and the freeze-guard harness can pin
|
|
13
|
+
* expectations.
|
|
14
|
+
*
|
|
15
|
+
* Rubric (each dimension 0–2, total / 10 → band):
|
|
16
|
+
*
|
|
17
|
+
* - `goal_clarity` — does the raw prompt name a single action verb +
|
|
18
|
+
* object + observable result?
|
|
19
|
+
* - `scope_boundary` — does the prompt name a file, class, module, or
|
|
20
|
+
* domain that bounds the change?
|
|
21
|
+
* - `ac_evidence` — did the refiner produce concrete, anchored
|
|
22
|
+
* acceptance criteria?
|
|
23
|
+
* - `stack_data` — does the prompt imply stack / data / migration work
|
|
24
|
+
* *and* identify the touched surface? (penalty if implied + unspecified)
|
|
25
|
+
* - `reversibility` — would a wrong reconstruction be cheaply rollback-
|
|
26
|
+
* able?
|
|
27
|
+
*
|
|
28
|
+
* Bands (per `agents/roadmaps/archive/road-to-prompt-driven-execution.md`):
|
|
29
|
+
*
|
|
30
|
+
* - `high` — score >= 0.8 → dispatcher proceeds silently
|
|
31
|
+
* - `medium` — 0.5 <= score < 0.8 → assumptions-report halt
|
|
32
|
+
* - `low` — score < 0.5 → one-question halt
|
|
33
|
+
*
|
|
34
|
+
* The scorer is the single source of truth for both the rubric and the
|
|
35
|
+
* band thresholds. Documentation (SKILL.md, ADR, contexts) cite this
|
|
36
|
+
* module — they do not re-derive the values.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
// --- Public types ------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Canonical dimension order. Matches the roadmap rubric and is the
|
|
43
|
+
* order `ConfidenceScore.dimensions` is rendered in by callers.
|
|
44
|
+
*/
|
|
45
|
+
export const DIMENSION_NAMES: readonly string[] = [
|
|
46
|
+
'goal_clarity',
|
|
47
|
+
'scope_boundary',
|
|
48
|
+
'ac_evidence',
|
|
49
|
+
'stack_data',
|
|
50
|
+
'reversibility',
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
/** Per-dimension ceiling. Five dimensions × 2 = 10 = full score. */
|
|
54
|
+
export const MAX_PER_DIMENSION = 2;
|
|
55
|
+
|
|
56
|
+
export const BAND_HIGH_MIN = 0.8;
|
|
57
|
+
export const BAND_MEDIUM_MIN = 0.5;
|
|
58
|
+
/**
|
|
59
|
+
* Band thresholds. Inclusive on the lower bound, exclusive on the upper.
|
|
60
|
+
*
|
|
61
|
+
* A score of exactly 0.8 lands in `high` (per roadmap: `high >= 0.8`);
|
|
62
|
+
* exactly 0.5 lands in `medium`. Anything below 0.5 is `low`.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Immutable result of one scoring pass.
|
|
67
|
+
*
|
|
68
|
+
* The Python source is a `@dataclass(frozen=True)`. We mirror the field set
|
|
69
|
+
* and the `field(default_factory=list)` default for `reasons` (each instance
|
|
70
|
+
* owns its own array). `Object.freeze` echoes `frozen=True` so callers cannot
|
|
71
|
+
* mutate a band after the dispatcher has routed on it.
|
|
72
|
+
*/
|
|
73
|
+
export class ConfidenceScore {
|
|
74
|
+
readonly band: string;
|
|
75
|
+
readonly score: number;
|
|
76
|
+
readonly dimensions: Record<string, number>;
|
|
77
|
+
readonly reasons: string[];
|
|
78
|
+
readonly ui_intent: boolean;
|
|
79
|
+
|
|
80
|
+
constructor(args: {
|
|
81
|
+
band: string;
|
|
82
|
+
score: number;
|
|
83
|
+
dimensions: Record<string, number>;
|
|
84
|
+
reasons?: string[];
|
|
85
|
+
ui_intent?: boolean;
|
|
86
|
+
}) {
|
|
87
|
+
this.band = args.band;
|
|
88
|
+
this.score = args.score;
|
|
89
|
+
this.dimensions = args.dimensions;
|
|
90
|
+
this.reasons = args.reasons ?? [];
|
|
91
|
+
this.ui_intent = args.ui_intent ?? false;
|
|
92
|
+
Object.freeze(this);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Heuristic vocabularies -------------------------------------------
|
|
97
|
+
|
|
98
|
+
const _ACTION_VERBS: ReadonlySet<string> = new Set([
|
|
99
|
+
'add', 'build', 'create', 'implement', 'introduce', 'write',
|
|
100
|
+
'fix', 'patch', 'repair', 'resolve',
|
|
101
|
+
'refactor', 'rename', 'extract', 'inline', 'split',
|
|
102
|
+
'remove', 'delete', 'drop', 'purge',
|
|
103
|
+
'update', 'upgrade', 'bump', 'migrate',
|
|
104
|
+
'optimize', 'speed', 'improve', 'tune',
|
|
105
|
+
'document', 'describe', 'explain',
|
|
106
|
+
'test', 'validate', 'verify',
|
|
107
|
+
'expose', 'publish', 'deprecate',
|
|
108
|
+
'configure', 'wire', 'connect',
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const _DOMAIN_NOUNS: ReadonlySet<string> = new Set([
|
|
112
|
+
'auth', 'authentication', 'authorization', 'login', 'logout', 'signup',
|
|
113
|
+
'user', 'users', 'account', 'profile',
|
|
114
|
+
'dashboard', 'search', 'checkout', 'cart', 'billing', 'payment',
|
|
115
|
+
'admin', 'settings', 'config',
|
|
116
|
+
'api', 'endpoint', 'webhook', 'queue', 'job', 'worker',
|
|
117
|
+
'frontend', 'backend', 'ui', 'view', 'page', 'form',
|
|
118
|
+
'database', 'migration', 'schema',
|
|
119
|
+
'report', 'export', 'import',
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const _STACK_DATA_KEYWORDS: ReadonlySet<string> = new Set([
|
|
123
|
+
'migration', 'schema', 'table', 'column', 'index',
|
|
124
|
+
'database', 'db', 'postgres', 'mysql', 'mariadb', 'sqlite',
|
|
125
|
+
'redis', 'cache', 'queue',
|
|
126
|
+
'dependency', 'package', 'library', 'framework', 'upgrade',
|
|
127
|
+
'breaking change', 'deprecate', 'api version',
|
|
128
|
+
'deploy', 'release',
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const _IRREVERSIBLE_KEYWORDS: ReadonlySet<string> = new Set([
|
|
132
|
+
'drop ', 'delete ', 'purge', 'wipe', 'truncate',
|
|
133
|
+
'send email', 'charge', 'refund', 'billing', 'payment', 'money',
|
|
134
|
+
'production data', 'live database', 'broadcast',
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const _UI_KEYWORDS: ReadonlySet<string> = new Set([
|
|
138
|
+
'redesign', 'color', 'colour', 'css', 'tailwind', 'layout',
|
|
139
|
+
'font', 'spacing', 'padding', 'margin', 'button', 'icon',
|
|
140
|
+
'responsive', 'mobile view', 'look', 'polish', 'prettier',
|
|
141
|
+
'theme', 'dark mode', 'light mode',
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
// Python `re.compile(...)` — the four alternatives, joined verbatim. The
|
|
145
|
+
// JS `RegExp` source mirrors the Python pattern character-for-character; the
|
|
146
|
+
// only escaping difference is `\\` in the PHP-namespace branch, which is a
|
|
147
|
+
// single backslash in the compiled pattern on both engines.
|
|
148
|
+
const _FILE_PATH_RE = new RegExp(
|
|
149
|
+
'`[^`]+`' + // backtick-wrapped tokens
|
|
150
|
+
'|[\\w./-]+\\.(?:py|php|ts|tsx|js|jsx|vue|blade\\.php|sql|yml|yaml|json|md)' +
|
|
151
|
+
'|[A-Z][\\w]+(?:\\\\[A-Z][\\w]+)+' + // PHP namespaces (Foo\Bar\Baz)
|
|
152
|
+
'|[A-Z][a-zA-Z]+::[a-zA-Z]+' + // PHP static call (Foo::bar)
|
|
153
|
+
'|[a-z]+(?:[A-Z][a-z]+){2,}', // camelCase identifiers w/ >=2 humps
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// --- Public API --------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Score a reconstructed prompt and return the band + rubric breakdown.
|
|
160
|
+
*
|
|
161
|
+
* @param raw The original user prompt text. Pre-stripped or not; the scorer
|
|
162
|
+
* normalises whitespace internally.
|
|
163
|
+
* @param ac Reconstructed acceptance criteria from the `refine-prompt`
|
|
164
|
+
* skill. `null`/`undefined` is treated as an empty list (no AC produced).
|
|
165
|
+
* @param assumptions Inferred assumptions surfaced by the refiner. Currently
|
|
166
|
+
* informational — the rubric does not penalise assumption count directly
|
|
167
|
+
* because medium-band halts are the resolution surface.
|
|
168
|
+
*/
|
|
169
|
+
export function score(args: {
|
|
170
|
+
raw: string;
|
|
171
|
+
ac?: string[] | null;
|
|
172
|
+
assumptions?: string[] | null;
|
|
173
|
+
}): ConfidenceScore {
|
|
174
|
+
const raw = args.raw;
|
|
175
|
+
// `(raw or "").strip()` — Python `str.strip()` removes leading/trailing
|
|
176
|
+
// whitespace per the `str.isspace` set; JS `trim()` covers the same
|
|
177
|
+
// Unicode whitespace for these inputs.
|
|
178
|
+
const text = (raw || '').trim();
|
|
179
|
+
const text_lower = text.toLowerCase();
|
|
180
|
+
const ac_list = [...(args.ac ?? [])];
|
|
181
|
+
|
|
182
|
+
const [g_val, g_reason] = _score_goal_clarity(text, text_lower);
|
|
183
|
+
const [s_val, s_reason] = _score_scope_boundary(text, text_lower);
|
|
184
|
+
const [e_val, e_reason] = _score_acceptance_evidence(ac_list);
|
|
185
|
+
const [k_val, k_reason] = _score_stack_data(text_lower);
|
|
186
|
+
const [r_val, r_reason] = _score_reversibility(text_lower);
|
|
187
|
+
|
|
188
|
+
const dimensions: Record<string, number> = {
|
|
189
|
+
goal_clarity: g_val,
|
|
190
|
+
scope_boundary: s_val,
|
|
191
|
+
ac_evidence: e_val,
|
|
192
|
+
stack_data: k_val,
|
|
193
|
+
reversibility: r_val,
|
|
194
|
+
};
|
|
195
|
+
const total = Object.values(dimensions).reduce((a, b) => a + b, 0);
|
|
196
|
+
const norm = _pyRound(total / (MAX_PER_DIMENSION * DIMENSION_NAMES.length), 4);
|
|
197
|
+
return new ConfidenceScore({
|
|
198
|
+
band: _band_from_score(norm),
|
|
199
|
+
score: norm,
|
|
200
|
+
dimensions,
|
|
201
|
+
reasons: [g_reason, s_reason, e_reason, k_reason, r_reason],
|
|
202
|
+
ui_intent: _detect_ui_intent(text_lower),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- Band mapping ------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Map a normalised score to one of `high` / `medium` / `low`.
|
|
210
|
+
*
|
|
211
|
+
* Inclusive on the lower bound to match the roadmap contract
|
|
212
|
+
* (`high >= 0.8`); a score of exactly `0.8` lands in `high`,
|
|
213
|
+
* exactly `0.5` in `medium`, anything below `0.5` in `low`.
|
|
214
|
+
*/
|
|
215
|
+
function _band_from_score(score_value: number): string {
|
|
216
|
+
if (score_value >= BAND_HIGH_MIN) {
|
|
217
|
+
return 'high';
|
|
218
|
+
}
|
|
219
|
+
if (score_value >= BAND_MEDIUM_MIN) {
|
|
220
|
+
return 'medium';
|
|
221
|
+
}
|
|
222
|
+
return 'low';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- Per-dimension scorers --------------------------------------------
|
|
226
|
+
|
|
227
|
+
/** Score whether the prompt names a single, observable outcome. */
|
|
228
|
+
function _score_goal_clarity(text: string, text_lower: string): [number, string] {
|
|
229
|
+
if (!text) {
|
|
230
|
+
return [0, 'goal_clarity=0: empty prompt'];
|
|
231
|
+
}
|
|
232
|
+
const has_verb = [..._ACTION_VERBS].some((v) =>
|
|
233
|
+
new RegExp(`\\b${_reEscape(v)}\\w*\\b`).test(text_lower),
|
|
234
|
+
);
|
|
235
|
+
// `text.rstrip().endswith("?")`
|
|
236
|
+
const is_question = _pyRstrip(text).endsWith('?');
|
|
237
|
+
// `len(text.split())` — Python `str.split()` with no args splits on runs
|
|
238
|
+
// of whitespace and drops empty tokens.
|
|
239
|
+
const word_count = _pySplitWhitespace(text).length;
|
|
240
|
+
const conjunction_split = /\b(and then|and also|plus)\b/.test(text_lower);
|
|
241
|
+
|
|
242
|
+
if (has_verb && !is_question && 4 <= word_count && word_count <= 40 && !conjunction_split) {
|
|
243
|
+
return [2, 'goal_clarity=2: action verb + bounded length + single outcome'];
|
|
244
|
+
}
|
|
245
|
+
if (has_verb && !is_question) {
|
|
246
|
+
if (conjunction_split) {
|
|
247
|
+
return [1, 'goal_clarity=1: verb present but multiple outcomes joined'];
|
|
248
|
+
}
|
|
249
|
+
return [1, 'goal_clarity=1: verb present but length is borderline'];
|
|
250
|
+
}
|
|
251
|
+
if (is_question) {
|
|
252
|
+
return [0, 'goal_clarity=0: prompt is a question, no executable verb'];
|
|
253
|
+
}
|
|
254
|
+
return [0, 'goal_clarity=0: no recognisable action verb'];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Score whether the prompt bounds the change to a concrete surface. */
|
|
258
|
+
function _score_scope_boundary(text: string, text_lower: string): [number, string] {
|
|
259
|
+
const has_path = _FILE_PATH_RE.test(text);
|
|
260
|
+
const has_domain = [..._DOMAIN_NOUNS].some((n) =>
|
|
261
|
+
new RegExp(`\\b${_reEscape(n)}\\b`).test(text_lower),
|
|
262
|
+
);
|
|
263
|
+
if (has_path) {
|
|
264
|
+
return [2, 'scope_boundary=2: explicit file/class/identifier named'];
|
|
265
|
+
}
|
|
266
|
+
if (has_domain) {
|
|
267
|
+
return [1, 'scope_boundary=1: domain noun present, no concrete path'];
|
|
268
|
+
}
|
|
269
|
+
return [0, 'scope_boundary=0: no file or domain anchor'];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Score the reconstructed AC list produced by the refiner. */
|
|
273
|
+
function _score_acceptance_evidence(ac: string[]): [number, string] {
|
|
274
|
+
const n = ac.length;
|
|
275
|
+
if (n === 0) {
|
|
276
|
+
return [0, 'ac_evidence=0: no acceptance criteria reconstructed'];
|
|
277
|
+
}
|
|
278
|
+
const anchored_signals = ['should', 'must', 'given', 'when', 'then', 'expect'];
|
|
279
|
+
let anchored = 0;
|
|
280
|
+
for (const line of ac) {
|
|
281
|
+
const low = line.toLowerCase();
|
|
282
|
+
if (anchored_signals.some((s) => low.includes(s))) {
|
|
283
|
+
anchored += 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (n >= 3 && anchored >= 2) {
|
|
287
|
+
return [2, `ac_evidence=2: ${n} criteria, ${anchored} anchored`];
|
|
288
|
+
}
|
|
289
|
+
if (n >= 1) {
|
|
290
|
+
return [1, `ac_evidence=1: ${n} criteria, ${anchored} anchored`];
|
|
291
|
+
}
|
|
292
|
+
return [0, 'ac_evidence=0: empty AC list'];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Penalise stack/data work that is implied but not bounded. */
|
|
296
|
+
function _score_stack_data(text_lower: string): [number, string] {
|
|
297
|
+
const implies_stack = [..._STACK_DATA_KEYWORDS].some((k) => text_lower.includes(k));
|
|
298
|
+
if (!implies_stack) {
|
|
299
|
+
return [2, 'stack_data=2: prompt is behavioural, no stack/data signal'];
|
|
300
|
+
}
|
|
301
|
+
const has_target = /\b(table|column|index|file|migration)\s+[`"\w]/.test(text_lower);
|
|
302
|
+
if (has_target) {
|
|
303
|
+
return [2, 'stack_data=2: stack/data work named with explicit target'];
|
|
304
|
+
}
|
|
305
|
+
return [0, 'stack_data=0: stack/data work implied without target'];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Score how cheaply a wrong reconstruction could be rolled back. */
|
|
309
|
+
function _score_reversibility(text_lower: string): [number, string] {
|
|
310
|
+
if ([..._IRREVERSIBLE_KEYWORDS].some((k) => text_lower.includes(k))) {
|
|
311
|
+
return [0, 'reversibility=0: irreversible keyword detected'];
|
|
312
|
+
}
|
|
313
|
+
const config_signals = ['config', 'env', 'secret', '.env', 'deploy'];
|
|
314
|
+
if (config_signals.some((s) => text_lower.includes(s))) {
|
|
315
|
+
return [1, 'reversibility=1: config/env surface, partial rollback cost'];
|
|
316
|
+
}
|
|
317
|
+
return [2, 'reversibility=2: code-only change, cheap to revert'];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Flag prompts that read as UI work for R3 routing. */
|
|
321
|
+
function _detect_ui_intent(text_lower: string): boolean {
|
|
322
|
+
return [..._UI_KEYWORDS].some((k) => text_lower.includes(k));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- Python-parity primitives -----------------------------------------
|
|
326
|
+
|
|
327
|
+
/** Python `re.escape` for the literal tokens in the heuristic vocabularies. */
|
|
328
|
+
function _reEscape(s: string): string {
|
|
329
|
+
// Python `re.escape` escapes every non-alphanumeric, non-underscore char.
|
|
330
|
+
// The vocab tokens are ASCII letters / spaces / dots, so this matches.
|
|
331
|
+
return s.replace(/[^a-zA-Z0-9_]/g, (ch) => '\\' + ch);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Python `str.rstrip()` (no arg) — strip trailing whitespace. */
|
|
335
|
+
function _pyRstrip(s: string): string {
|
|
336
|
+
return s.replace(/\s+$/u, '');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Python `str.split()` (no arg) — split on whitespace runs, drop empties. */
|
|
340
|
+
function _pySplitWhitespace(s: string): string[] {
|
|
341
|
+
const trimmed = s.trim();
|
|
342
|
+
if (trimmed === '') {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
return trimmed.split(/\s+/u);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Python 3 `round(x, ndigits)` — round-half-to-even (banker's rounding) on
|
|
350
|
+
* the exact decimal expansion of the IEEE-754 double. Same algorithm as
|
|
351
|
+
* `value_ladder.ts::pyRound` / `report_renderer.ts::_pyRound`. Inlined to
|
|
352
|
+
* keep the consumer-shipped module free of cross-tree imports (ADR-200 §7).
|
|
353
|
+
*/
|
|
354
|
+
function _pyRound(value: number, ndigits = 0): number {
|
|
355
|
+
if (!Number.isFinite(value) || value === 0) {
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
const sign = value < 0 ? -1 : 1;
|
|
359
|
+
const abs = Math.abs(value);
|
|
360
|
+
const str = abs.toPrecision(17);
|
|
361
|
+
if (str.includes('e') || str.includes('E')) {
|
|
362
|
+
const factor = 10 ** ndigits;
|
|
363
|
+
return value > 0
|
|
364
|
+
? (Math.round(abs * factor) / factor) * sign
|
|
365
|
+
: -Math.round(abs * factor) / factor;
|
|
366
|
+
}
|
|
367
|
+
let [intPart, fracPart = ''] = str.split('.');
|
|
368
|
+
while (fracPart.length <= ndigits) {
|
|
369
|
+
fracPart += '0';
|
|
370
|
+
}
|
|
371
|
+
const keep = fracPart.slice(0, ndigits);
|
|
372
|
+
const deciding = fracPart[ndigits] ?? '0';
|
|
373
|
+
const rest = fracPart.slice(ndigits + 1).replace(/0+$/, '');
|
|
374
|
+
let digits = (intPart + keep).replace(/^0+(?=\d)/, '');
|
|
375
|
+
let roundUp = false;
|
|
376
|
+
if (deciding > '5' || (deciding === '5' && rest !== '')) {
|
|
377
|
+
roundUp = true;
|
|
378
|
+
} else if (deciding === '5' && rest === '') {
|
|
379
|
+
// exact half → round to even
|
|
380
|
+
const lastKept = digits.length > 0 ? (digits[digits.length - 1] ?? '0') : '0';
|
|
381
|
+
roundUp = (lastKept.charCodeAt(0) - 48) % 2 === 1;
|
|
382
|
+
}
|
|
383
|
+
if (roundUp) {
|
|
384
|
+
digits = _incrementDecimalString(digits);
|
|
385
|
+
}
|
|
386
|
+
// Reinsert the decimal point `ndigits` places from the right.
|
|
387
|
+
let result: number;
|
|
388
|
+
if (ndigits === 0) {
|
|
389
|
+
result = Number(digits);
|
|
390
|
+
} else {
|
|
391
|
+
const padded = digits.padStart(ndigits + 1, '0');
|
|
392
|
+
const cut = padded.length - ndigits;
|
|
393
|
+
const intStr = padded.slice(0, cut) || '0';
|
|
394
|
+
const fracStr = padded.slice(cut);
|
|
395
|
+
result = Number(`${intStr}.${fracStr}`);
|
|
396
|
+
}
|
|
397
|
+
return result * sign;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Add one to a non-negative integer represented as a decimal string. */
|
|
401
|
+
function _incrementDecimalString(s: string): string {
|
|
402
|
+
const chars = (s === '' ? '0' : s).split('');
|
|
403
|
+
let i = chars.length - 1;
|
|
404
|
+
while (i >= 0) {
|
|
405
|
+
const ch = chars[i] ?? '0';
|
|
406
|
+
if (ch === '9') {
|
|
407
|
+
chars[i] = '0';
|
|
408
|
+
i -= 1;
|
|
409
|
+
} else {
|
|
410
|
+
chars[i] = String.fromCharCode(ch.charCodeAt(0) + 1);
|
|
411
|
+
return chars.join('');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return '1' + chars.join('');
|
|
415
|
+
}
|