@event4u/agent-config 17.0.0 → 17.1.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 +104 -485
- package/README.md +2 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +5 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +5 -1
- package/dist/cli-delegate/{chunk-IROAQ77A.js → chunk-3PK3VZHB.js} +107 -9
- package/dist/cli-delegate/{chunk-2SFOOFJJ.js → chunk-BZJ4Y3CI.js} +1 -1
- package/dist/cli-delegate/{chunk-CAL76ADI.js → chunk-CMZEKSNN.js} +52 -10
- package/dist/cli-delegate/{chunk-ESUNQ5XR.js → chunk-E3CFULNX.js} +22 -4
- package/dist/cli-delegate/{chunk-GOMILC3O.js → chunk-QBHEI5N4.js} +4 -3
- package/dist/cli-delegate/{chunk-SIOMDAKL.js → chunk-VYZGJLCA.js} +29 -39
- package/dist/cli-delegate/chunk-W5JXMF2T.js +77 -0
- package/dist/cli-delegate/{chunk-4SHNERBL.js → chunk-XIAPY44N.js} +1 -1
- package/dist/cli-delegate/{chunk-JHMMWNJ2.js → chunk-YHAPONWV.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +9 -8
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_preflight.js +3 -3
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +9 -13
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- 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 +98 -74
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/install/claudeRuleRewrite.js +10 -3
- package/dist/install/conflict.js +24 -2
- package/dist/install/conflictTracker.js +14 -3
- package/dist/install/install.mjs +222 -117
- package/dist/install/installThinLayer.js +1 -0
- package/dist/install/loggedWrites.js +142 -0
- package/dist/install/paths.js +12 -2
- package/dist/install/preserve.js +25 -4
- package/dist/install/txlog.js +10 -6
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +845 -294
- package/dist/scripts/_lib/egress_rules.js +4 -1
- package/dist/scripts/_lib/identifier_redaction.js +35 -2
- package/dist/scripts/ai_council/seat_policy.js +7 -1
- package/dist/server/schemas/settings.js +1 -1
- package/package.json +1 -1
- package/src/config/ci-local-parity.yml +1 -1
- package/src/config/gate-coverage.yml +27 -7
- package/src/config/gate-violation-baselines.json +8 -2
- package/src/config/hook-bundle-budget.json +2 -2
- package/src/config/production-entries.json +18 -0
- package/src/config/release-gate-locality.yml +0 -7
- package/src/install/claudeRuleRewrite.ts +10 -1
- package/src/install/conflict.ts +24 -1
- package/src/install/conflictTracker.ts +18 -2
- package/src/install/installThinLayer.ts +6 -0
- package/src/install/loggedWrites.ts +148 -0
- package/src/install/paths.ts +11 -2
- package/src/install/preserve.ts +40 -4
- package/src/install/txlog.ts +10 -6
- package/src/scripts/_cli/cmd_conformance.ts +3 -2
- package/src/scripts/_cli/cmd_update.ts +15 -17
- package/src/scripts/_lib/council_settings_block.ts +9 -2
- package/src/scripts/_lib/egress_rules.ts +4 -1
- package/src/scripts/_lib/host_capability.ts +10 -5
- package/src/scripts/_lib/identifier_redaction.ts +38 -2
- package/src/scripts/_lib/price_freshness.ts +112 -0
- package/src/scripts/_lib/production_reach.ts +194 -0
- package/src/scripts/_lib/quota_parked.ts +17 -28
- package/src/scripts/_lib/release_findings_ingest.ts +13 -335
- package/src/scripts/_lib/tier_downshift_verdict.ts +81 -35
- package/src/scripts/ai_council/chairman.ts +5 -2
- package/src/scripts/ai_council/clients.ts +4 -3
- package/src/scripts/ai_council/free_hand_shadow.ts +24 -12
- package/src/scripts/ai_council/free_route_catalog.ts +142 -5
- package/src/scripts/ai_council/free_route_client.ts +4 -0
- package/src/scripts/ai_council/hand_payload.ts +115 -7
- package/src/scripts/ai_council/orchestrator.ts +7 -10
- package/src/scripts/ai_council/route_chooser.ts +1 -1
- package/src/scripts/ai_council/route_member.ts +66 -4
- package/src/scripts/ai_council/seat_policy.ts +8 -1
- package/src/scripts/ai_council/self_hosted_probe.ts +327 -0
- package/src/scripts/ai_council/status_surface.ts +2 -0
- package/src/scripts/ask_transport.ts +137 -15
- package/src/scripts/check_finding_dispositions.ts +25 -71
- package/src/scripts/check_release_pr_shape.ts +3 -3
- package/src/scripts/check_step_production_reach.ts +257 -0
- package/src/scripts/council_cli.ts +10 -8
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +3 -0
- package/src/scripts/hooks/dispatch_hook.ts +2 -2
- package/src/scripts/hooks/payload_stub.ts +27 -0
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +29 -70
- package/src/scripts/hooks/subagent_ledger_hook.ts +80 -0
- package/src/scripts/install.ts +2 -2
- package/src/scripts/lint_decision_classes.ts +34 -0
- package/src/scripts/lint_hook_manifest.ts +24 -0
- package/src/scripts/lint_roadmap_blockers.ts +52 -3
- package/src/scripts/release.ts +5 -9
- package/src/scripts/release_drill.ts +11 -165
- package/src/scripts/release_publication.ts +0 -9
- package/src/scripts/report_production_reach.ts +35 -0
- package/src/scripts/worker_reuse_shadow_report.ts +399 -1
- package/src/server/schemas/settings.ts +1 -1
- package/dist/cli-delegate/chunk-A4QJ6ENT.js +0 -50
- /package/dist/cli-delegate/{chunk-E6XNBVE4.js → chunk-KR5ZIFPO.js} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A price row is evidence with a date, and it expires.
|
|
3
|
+
*
|
|
4
|
+
* `internal/bench/pricing.yaml` priced a `claude-haiku-5-5` leg with a row sourced
|
|
5
|
+
* 2026-05-14 for a former Haiku generation at 2.5x the served model's rate, and the
|
|
6
|
+
* cost verdict read it without complaint. A row now refuses to price when it is
|
|
7
|
+
* older than PRICE_MAX_AGE_DAYS, when it names no source model (nothing vouches for
|
|
8
|
+
* its generation), when its source model is an older generation than the served
|
|
9
|
+
* one, or when it belongs to another model family. A refusal is a reason string,
|
|
10
|
+
* never a zero price.
|
|
11
|
+
*/
|
|
12
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
13
|
+
|
|
14
|
+
export const PRICE_MAX_AGE_DAYS = 120;
|
|
15
|
+
|
|
16
|
+
const FAMILIES = ['haiku', 'sonnet', 'opus', 'fable', 'mythos'] as const;
|
|
17
|
+
|
|
18
|
+
/** USD per million tokens. */
|
|
19
|
+
export interface Rates {
|
|
20
|
+
input: number;
|
|
21
|
+
output: number;
|
|
22
|
+
cache_write: number;
|
|
23
|
+
cache_read: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PriceRow extends Rates {
|
|
27
|
+
/** Lookup key: a family (`haiku`) or a full model id (`claude-haiku-5-5`). */
|
|
28
|
+
tier: string;
|
|
29
|
+
/** ISO date the rates were read from `source`. */
|
|
30
|
+
sourced_on: string;
|
|
31
|
+
source?: string;
|
|
32
|
+
/** The model whose published rates these are. */
|
|
33
|
+
model?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function familyOf(id: string): string | null {
|
|
37
|
+
const m = id.toLowerCase();
|
|
38
|
+
return FAMILIES.find((f) => m.includes(f)) ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `claude-haiku-5-5` → 5.5, `claude-fable-5` → 5; a trailing 8-digit date is not a minor. */
|
|
42
|
+
export function modelGeneration(id: string): number | null {
|
|
43
|
+
const m = id.toLowerCase().match(/(?:haiku|sonnet|opus|fable|mythos)-(\d{1,2})(?:-(\d{1,2}))?(?![0-9])/);
|
|
44
|
+
if (!m) return null;
|
|
45
|
+
return Number(m[1]) + (m[2] === undefined ? 0 : Number(m[2]) / 10);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DAY_MS = 86_400_000;
|
|
49
|
+
|
|
50
|
+
export function priceRefusal(row: PriceRow, servedModel: string, asOf: Date): string | null {
|
|
51
|
+
const sourced = /^\d{4}-\d{2}-\d{2}$/.test(row.sourced_on) ? Date.parse(`${row.sourced_on}T00:00:00Z`) : Number.NaN;
|
|
52
|
+
if (Number.isNaN(sourced)) return `price row ${row.tier} carries no readable sourced_on date`;
|
|
53
|
+
const ageDays = Math.floor((asOf.getTime() - sourced) / DAY_MS);
|
|
54
|
+
if (ageDays > PRICE_MAX_AGE_DAYS) {
|
|
55
|
+
return `price row ${row.tier} is ${ageDays} days old (sourced ${row.sourced_on}), beyond the ${PRICE_MAX_AGE_DAYS}-day bound`;
|
|
56
|
+
}
|
|
57
|
+
const servedFamily = familyOf(servedModel);
|
|
58
|
+
if (servedFamily === null || familyOf(row.model ?? row.tier) !== servedFamily) {
|
|
59
|
+
return `price row ${row.tier} is not of the family of served model ${servedModel}`;
|
|
60
|
+
}
|
|
61
|
+
if (!row.model) return `price row ${row.tier} names no source model, so nothing vouches for its generation`;
|
|
62
|
+
const rowGen = modelGeneration(row.model);
|
|
63
|
+
const servedGen = modelGeneration(servedModel);
|
|
64
|
+
if (rowGen === null || servedGen === null) return `cannot read the generation of ${row.model} or ${servedModel}`;
|
|
65
|
+
if (servedGen > rowGen) {
|
|
66
|
+
return `price row ${row.tier} is sourced for generation ${rowGen} (${row.model}); served model ${servedModel} is generation ${servedGen}`;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The served model's own row first, then its family's; the first refusal is reported. */
|
|
72
|
+
export function selectPrice(rows: readonly PriceRow[], servedModel: string, asOf: Date): { price: Rates; row: PriceRow } | { refusal: string } {
|
|
73
|
+
const id = servedModel.toLowerCase();
|
|
74
|
+
const family = familyOf(id);
|
|
75
|
+
const candidates = [...rows.filter((r) => r.tier.toLowerCase() === id), ...rows.filter((r) => family !== null && r.tier.toLowerCase() === family)];
|
|
76
|
+
if (candidates.length === 0) return { refusal: `no price row for served model ${servedModel}` };
|
|
77
|
+
let first: string | null = null;
|
|
78
|
+
for (const row of candidates) {
|
|
79
|
+
const why = priceRefusal(row, servedModel, asOf);
|
|
80
|
+
if (why === null) {
|
|
81
|
+
return { price: { input: row.input, output: row.output, cache_write: row.cache_write, cache_read: row.cache_read }, row };
|
|
82
|
+
}
|
|
83
|
+
first ??= why;
|
|
84
|
+
}
|
|
85
|
+
return { refusal: first! };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isoDate(v: unknown): string {
|
|
89
|
+
if (v instanceof Date) return v.toISOString().slice(0, 10);
|
|
90
|
+
return typeof v === 'string' ? v : '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Rows of a `pricing.yaml` document; a row without a tier is skipped. */
|
|
94
|
+
export function parsePriceRows(yamlText: string): PriceRow[] {
|
|
95
|
+
const doc = yamlLoad(yamlText) as { models?: unknown } | null;
|
|
96
|
+
const models = Array.isArray(doc?.models) ? (doc.models as Record<string, unknown>[]) : [];
|
|
97
|
+
const rows: PriceRow[] = [];
|
|
98
|
+
for (const m of models) {
|
|
99
|
+
if (!m || typeof m['tier'] !== 'string') continue;
|
|
100
|
+
rows.push({
|
|
101
|
+
tier: m['tier'],
|
|
102
|
+
...(typeof m['model'] === 'string' ? { model: m['model'] } : {}),
|
|
103
|
+
sourced_on: isoDate(m['sourced_on']),
|
|
104
|
+
...(typeof m['source'] === 'string' ? { source: m['source'] } : {}),
|
|
105
|
+
input: Number(m['input']),
|
|
106
|
+
output: Number(m['output']),
|
|
107
|
+
cache_write: Number(m['cache_write']),
|
|
108
|
+
cache_read: Number(m['cache_read']),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return rows;
|
|
112
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which `src/` symbols a production entry reaches (road-to-a-tick-the-shipped-path-earns 3.1).
|
|
3
|
+
*
|
|
4
|
+
* Static and name-level, the same reading as `dispatch_import_closure`: relative specifiers only,
|
|
5
|
+
* dynamic `import()` not followed. A symbol counts as reached when some module
|
|
6
|
+
* in the closure imports it BY NAME from its defining module (or imports that
|
|
7
|
+
* module whole), or when the defining module is itself an entry. Reaching the
|
|
8
|
+
* module is not enough — `egress_normalize.ts` is in the closure for one regex
|
|
9
|
+
* while `detectNormalized` has no production importer, and that is the case
|
|
10
|
+
* this exists to see.
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
|
|
16
|
+
export const ENTRIES_CONFIG_REL = 'src/config/production-entries.json';
|
|
17
|
+
|
|
18
|
+
export interface ReachTree {
|
|
19
|
+
list(): readonly string[];
|
|
20
|
+
read(rel: string): string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The checkout under `root`: tracked plus untracked-not-ignored files. */
|
|
24
|
+
export function worktreeOf(root: string): ReachTree {
|
|
25
|
+
let files: string[] | null = null;
|
|
26
|
+
return {
|
|
27
|
+
list() {
|
|
28
|
+
if (files === null) {
|
|
29
|
+
const res = spawnSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], {
|
|
30
|
+
cwd: root,
|
|
31
|
+
encoding: 'utf-8',
|
|
32
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
33
|
+
});
|
|
34
|
+
files = res.status === 0 ? res.stdout.split('\n').filter((l) => l !== '') : [];
|
|
35
|
+
}
|
|
36
|
+
return files;
|
|
37
|
+
},
|
|
38
|
+
read(rel) {
|
|
39
|
+
const abs = path.join(root, rel);
|
|
40
|
+
return fs.existsSync(abs) && fs.statSync(abs).isFile() ? fs.readFileSync(abs, 'utf-8') : null;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A commit's tree, read from objects — for replaying an old range. */
|
|
46
|
+
export function commitTreeOf(root: string, ref: string): ReachTree {
|
|
47
|
+
const ls = spawnSync('git', ['ls-tree', '-r', '--name-only', ref], { cwd: root, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 });
|
|
48
|
+
const files = ls.status === 0 ? ls.stdout.split('\n').filter((l) => l !== '') : [];
|
|
49
|
+
const present = new Set(files);
|
|
50
|
+
return {
|
|
51
|
+
list: () => files,
|
|
52
|
+
read(rel) {
|
|
53
|
+
if (!present.has(rel)) return null;
|
|
54
|
+
const res = spawnSync('git', ['show', `${ref}:${rel}`], { cwd: root, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 });
|
|
55
|
+
return res.status === 0 ? res.stdout : null;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function globToRegExp(glob: string): RegExp {
|
|
61
|
+
let re = '';
|
|
62
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
63
|
+
const c = glob[i] as string;
|
|
64
|
+
if (c === '*' && glob[i + 1] === '*') {
|
|
65
|
+
re += glob[i + 2] === '/' ? '(?:.*/)?' : '.*';
|
|
66
|
+
i += glob[i + 2] === '/' ? 2 : 1;
|
|
67
|
+
} else if (c === '*') re += '[^/]*';
|
|
68
|
+
else re += c.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
69
|
+
}
|
|
70
|
+
return new RegExp(`^${re}$`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Resolve a `.js` specifier from `fromRel` to the source it names in `tree`. */
|
|
74
|
+
function resolve(tree: ReachTree, fromRel: string, spec: string): string | null {
|
|
75
|
+
const base = path.posix.join(path.posix.dirname(fromRel), spec);
|
|
76
|
+
for (const c of [base.replace(/\.js$/, '.ts'), base, `${base}.ts`, `${base}/index.ts`]) {
|
|
77
|
+
const norm = path.posix.normalize(c);
|
|
78
|
+
if (tree.read(norm) !== null) return norm;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `{ a, b as c }` → `['a', 'b']`; `* as ns` and a default import → `['*']`. */
|
|
84
|
+
export function importedNames(clause: string): string[] {
|
|
85
|
+
const names: string[] = [];
|
|
86
|
+
const braces = /\{([^}]*)\}/.exec(clause);
|
|
87
|
+
if (braces) {
|
|
88
|
+
for (const part of (braces[1] as string).split(',')) {
|
|
89
|
+
const name = part.trim().replace(/^type\s+/, '').split(/\s+as\s+/)[0]?.trim();
|
|
90
|
+
if (name) names.push(name);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const outside = clause.replace(/\{[^}]*\}/, '');
|
|
94
|
+
if (/\*\s+as\s+\w+/.test(outside) || /^\s*(?:type\s+)?[A-Za-z_$][\w$]*\s*(?:,|$)/.test(outside)) names.push('*');
|
|
95
|
+
return names;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ModuleImport {
|
|
99
|
+
readonly target: string;
|
|
100
|
+
readonly names: readonly string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Every relative import of `rel`, resolved, with the names it takes. */
|
|
104
|
+
export function moduleImports(tree: ReachTree, rel: string): ModuleImport[] {
|
|
105
|
+
const source = (tree.read(rel) ?? '').replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
106
|
+
const out: ModuleImport[] = [];
|
|
107
|
+
const re = /\b(import|export)\s+(?:type\s+)?((?:(?!\b(?:import|export)\b)[^;'"])*?)\bfrom\s*['"](\.{1,2}\/[^'"]+)['"]|\bimport\s*['"](\.{1,2}\/[^'"]+)['"]/g;
|
|
108
|
+
let m: RegExpExecArray | null;
|
|
109
|
+
while ((m = re.exec(source)) !== null) {
|
|
110
|
+
const spec = (m[3] ?? m[4]) as string;
|
|
111
|
+
const target = resolve(tree, rel, spec);
|
|
112
|
+
if (target === null) continue;
|
|
113
|
+
out.push({ target, names: m[4] !== undefined ? [] : importedNames(m[2] ?? '') });
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Entries from the committed config: the fixed list plus every script an entry source names. */
|
|
119
|
+
export function productionEntries(tree: ReachTree): string[] {
|
|
120
|
+
const raw = tree.read(ENTRIES_CONFIG_REL);
|
|
121
|
+
if (raw === null) return [];
|
|
122
|
+
const cfg = JSON.parse(raw) as { entries?: string[]; entry_sources?: string[] };
|
|
123
|
+
const entries = new Set((cfg.entries ?? []).filter((e) => tree.read(e) !== null));
|
|
124
|
+
const sources = (cfg.entry_sources ?? []).map(globToRegExp);
|
|
125
|
+
for (const file of tree.list()) {
|
|
126
|
+
if (!sources.some((re) => re.test(file))) continue;
|
|
127
|
+
for (const m of (tree.read(file) ?? '').matchAll(/(?:src\/)?scripts\/([A-Za-z0-9_/.-]+?)(?:\.ts)?(?![A-Za-z0-9_/.-])/g)) {
|
|
128
|
+
const candidate = `src/scripts/${m[1] as string}.ts`;
|
|
129
|
+
if (tree.read(candidate) !== null) entries.add(candidate);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return [...entries].sort();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Exports the module's own code also uses — named again after their declaration.
|
|
137
|
+
* A reached module's `main` runs its helpers without importing them, so a helper
|
|
138
|
+
* a concern calls is reached; one only its test calls is not.
|
|
139
|
+
*/
|
|
140
|
+
export function usedExports(source: string): string[] {
|
|
141
|
+
const code = source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
142
|
+
const out: string[] = [];
|
|
143
|
+
for (const m of code.matchAll(/\bexport\s+(?:declare\s+)?(?:async\s+)?(?:function\*?|const|let|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g)) {
|
|
144
|
+
const name = m[1] as string;
|
|
145
|
+
const uses = code.match(new RegExp(`(?<![\\w$.])${name.replace(/\$/g, '\\$')}(?![\\w$])`, 'g'))?.length ?? 0;
|
|
146
|
+
if (uses >= 2) out.push(name);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface Reach {
|
|
152
|
+
readonly entries: ReadonlySet<string>;
|
|
153
|
+
readonly modules: ReadonlySet<string>;
|
|
154
|
+
/** `<module>#<name>`; `<module>#*` when a closure module takes it whole. */
|
|
155
|
+
readonly symbols: ReadonlySet<string>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function productionReach(tree: ReachTree, extra: readonly string[] = []): Reach {
|
|
159
|
+
const entries = new Set([...productionEntries(tree), ...extra.filter((e) => tree.read(e) !== null)]);
|
|
160
|
+
const modules = new Set<string>();
|
|
161
|
+
const symbols = new Set<string>();
|
|
162
|
+
const queue = [...entries];
|
|
163
|
+
while (queue.length > 0) {
|
|
164
|
+
const rel = queue.shift() as string;
|
|
165
|
+
if (modules.has(rel)) continue;
|
|
166
|
+
modules.add(rel);
|
|
167
|
+
for (const name of usedExports(tree.read(rel) ?? '')) symbols.add(`${rel}#${name}`);
|
|
168
|
+
for (const imp of moduleImports(tree, rel)) {
|
|
169
|
+
for (const n of imp.names) symbols.add(`${imp.target}#${n}`);
|
|
170
|
+
if (!modules.has(imp.target)) queue.push(imp.target);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { entries, modules, symbols };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The `src/` symbols a test takes, and whether production reaches at least one. */
|
|
177
|
+
export function testReaches(tree: ReachTree, reach: Reach, testRel: string): { taken: string[]; reached: string[] } {
|
|
178
|
+
const taken: string[] = [];
|
|
179
|
+
const reached: string[] = [];
|
|
180
|
+
for (const imp of moduleImports(tree, testRel)) {
|
|
181
|
+
if (!imp.target.startsWith('src/') || /\.test\.ts$/.test(imp.target)) continue;
|
|
182
|
+
for (const n of imp.names.length ? imp.names : ['*']) {
|
|
183
|
+
const key = `${imp.target}#${n}`;
|
|
184
|
+
taken.push(key);
|
|
185
|
+
const hit =
|
|
186
|
+
reach.entries.has(imp.target) ||
|
|
187
|
+
reach.symbols.has(key) ||
|
|
188
|
+
reach.symbols.has(`${imp.target}#*`) ||
|
|
189
|
+
(n === '*' && reach.modules.has(imp.target));
|
|
190
|
+
if (hit) reached.push(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { taken, reached };
|
|
194
|
+
}
|
|
@@ -60,7 +60,9 @@ export function markerFile(repoRoot: string, runId: string): string {
|
|
|
60
60
|
*
|
|
61
61
|
* Idempotent on the timestamp: a second park inside one run keeps the original
|
|
62
62
|
* `parked_at` and merges the provider set, so "when did this run first hit the
|
|
63
|
-
* cliff" survives a round that parks twice.
|
|
63
|
+
* cliff" survives a round that parks twice. A host park is the exception: it
|
|
64
|
+
* sets its own `source` and refreshes `parked_at`, since the host's rate window
|
|
65
|
+
* starts at its own signal, not at an earlier council park.
|
|
64
66
|
*/
|
|
65
67
|
export function writeQuotaParked(
|
|
66
68
|
repoRoot: string,
|
|
@@ -71,12 +73,13 @@ export function writeQuotaParked(
|
|
|
71
73
|
): QuotaParkedMarker {
|
|
72
74
|
const file = markerFile(repoRoot, runId);
|
|
73
75
|
const existing = readMarker(file);
|
|
76
|
+
const host = source === 'host';
|
|
74
77
|
const merged = [...new Set([...(existing?.providers ?? []), ...providers])].sort();
|
|
75
78
|
const marker: QuotaParkedMarker = {
|
|
76
79
|
run_id: runId,
|
|
77
80
|
providers: merged,
|
|
78
|
-
parked_at: existing?.parked_at ?? now().toISOString(),
|
|
79
|
-
source: existing?.source ?? source,
|
|
81
|
+
parked_at: (host ? null : existing?.parked_at) ?? now().toISOString(),
|
|
82
|
+
source: host ? source : (existing?.source ?? source),
|
|
80
83
|
};
|
|
81
84
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
82
85
|
fs.writeFileSync(file, `${JSON.stringify(marker, null, 2)}\n`, 'utf8');
|
|
@@ -106,20 +109,7 @@ export function clearQuotaParked(repoRoot: string, runId: string): void {
|
|
|
106
109
|
* per parked run, so the scan is cheap by construction.
|
|
107
110
|
*/
|
|
108
111
|
export function findQuotaParked(repoRoot: string, sessionId: string): QuotaParkedMarker | null {
|
|
109
|
-
|
|
110
|
-
if (direct !== null) return direct;
|
|
111
|
-
let names: string[];
|
|
112
|
-
try {
|
|
113
|
-
names = fs.readdirSync(path.join(repoRoot, QUOTA_PARKED_DIR_REL));
|
|
114
|
-
} catch {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
for (const name of names) {
|
|
118
|
-
if (!name.endsWith('.json')) continue;
|
|
119
|
-
const m = readMarker(path.join(repoRoot, QUOTA_PARKED_DIR_REL, name));
|
|
120
|
-
if (m !== null && m.run_id === sessionId) return m;
|
|
121
|
-
}
|
|
122
|
-
return null;
|
|
112
|
+
return readMarker(markerFile(repoRoot, sessionId)) ?? allMarkers(repoRoot).find((m) => m.run_id === sessionId) ?? null;
|
|
123
113
|
}
|
|
124
114
|
|
|
125
115
|
/** The plan's rolling rate window; a host marker older than this no longer describes it. */
|
|
@@ -127,20 +117,19 @@ export const HOST_RATE_WINDOW_MS = 5 * 3_600_000;
|
|
|
127
117
|
|
|
128
118
|
/** True when a host-sourced marker was parked inside the rate window ending at `nowMs`. */
|
|
129
119
|
export function recentHostMarker(repoRoot: string, nowMs: number = Date.now(), windowMs: number = HOST_RATE_WINDOW_MS): boolean {
|
|
130
|
-
|
|
120
|
+
return allMarkers(repoRoot).some((m) => {
|
|
121
|
+
const age = nowMs - Date.parse(m.parked_at);
|
|
122
|
+
return m.source === 'host' && age >= 0 && age < windowMs;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function allMarkers(repoRoot: string): QuotaParkedMarker[] {
|
|
127
|
+
const dir = path.join(repoRoot, QUOTA_PARKED_DIR_REL);
|
|
131
128
|
try {
|
|
132
|
-
|
|
129
|
+
return fs.readdirSync(dir).flatMap((n) => (n.endsWith('.json') ? (readMarker(path.join(dir, n)) ?? []) : []));
|
|
133
130
|
} catch {
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
for (const name of names) {
|
|
137
|
-
if (!name.endsWith('.json')) continue;
|
|
138
|
-
const m = readMarker(path.join(repoRoot, QUOTA_PARKED_DIR_REL, name));
|
|
139
|
-
if (m === null || m.source !== 'host') continue;
|
|
140
|
-
const at = Date.parse(m.parked_at);
|
|
141
|
-
if (Number.isFinite(at) && nowMs - at >= 0 && nowMs - at < windowMs) return true;
|
|
131
|
+
return [];
|
|
142
132
|
}
|
|
143
|
-
return false;
|
|
144
133
|
}
|
|
145
134
|
|
|
146
135
|
function readMarker(file: string): QuotaParkedMarker | null {
|