@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,868 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* One-shot memory observability report.
|
|
4
|
+
*
|
|
5
|
+
* TypeScript twin of `src/agent-src/templates/scripts/memory_report.py`
|
|
6
|
+
* (ADR-200, consumer-template memory). The public API and CLI contract
|
|
7
|
+
* mirror the Python original EXACTLY — same exported names (snake_case
|
|
8
|
+
* kept deliberately), same exit codes, stdout/stderr split, byte-identical
|
|
9
|
+
* messages, and
|
|
10
|
+
* byte-identical generated output (text report + `--format json`,
|
|
11
|
+
* matching json.dumps(indent=2, default=str)). No behaviour changes —
|
|
12
|
+
* latent Python bugs are replicated and flagged as divergence candidates.
|
|
13
|
+
*
|
|
14
|
+
* Shows:
|
|
15
|
+
* - Backend status (via `memory_status.status()`)
|
|
16
|
+
* - Intake counts per entry type and per month
|
|
17
|
+
* - Curated file staleness (files with `last_validated` older than
|
|
18
|
+
* `review_after_days` for any entry)
|
|
19
|
+
* - Supersede-chain totals
|
|
20
|
+
*
|
|
21
|
+
* Exit code: 0 = report printed (green or otherwise); 2 = PyYAML missing.
|
|
22
|
+
* `task memory:status` is the expected entry point, but the script is
|
|
23
|
+
* usable standalone.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* memory_report
|
|
27
|
+
* memory_report --format json
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import process from 'node:process';
|
|
33
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
34
|
+
import YAML, { parseDocument } from 'yaml';
|
|
35
|
+
|
|
36
|
+
import * as memory_status from './memory_status.js';
|
|
37
|
+
|
|
38
|
+
// Mutable so tests can repoint them at a tmp tree (monkeypatch parity).
|
|
39
|
+
export let MEMORY_ROOT = path.join('agents', 'memory');
|
|
40
|
+
export let INTAKE_ROOT = path.join(MEMORY_ROOT, 'intake');
|
|
41
|
+
|
|
42
|
+
/** Test-only setters mirroring pytest monkeypatch on the module constants. */
|
|
43
|
+
export function _setMemoryRoot(p: string): void {
|
|
44
|
+
MEMORY_ROOT = p;
|
|
45
|
+
}
|
|
46
|
+
export function _setIntakeRoot(p: string): void {
|
|
47
|
+
INTAKE_ROOT = p;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const CURATED_TYPES: readonly string[] = [
|
|
51
|
+
'ownership',
|
|
52
|
+
'historical-patterns',
|
|
53
|
+
'domain-invariants',
|
|
54
|
+
'incident-learnings',
|
|
55
|
+
'product-rules',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Role-mode marker grepped from session captures / reports / handoffs.
|
|
59
|
+
// Matches `<!-- role-mode: <slug> | contract: ... -->` on any single line.
|
|
60
|
+
const _MODE_MARKER_PATTERN = /<!--\s*role-mode:\s*([a-z0-9][a-z0-9-]*)\s*\|\s*contract:[^>]*-->/g;
|
|
61
|
+
const _MODE_SCAN_DIRS: readonly string[] = [
|
|
62
|
+
path.join('agents', 'sessions'),
|
|
63
|
+
path.join('agents', 'runtime', 'reports'),
|
|
64
|
+
path.join('agents', 'handoffs'),
|
|
65
|
+
path.join('agents', 'learnings'),
|
|
66
|
+
];
|
|
67
|
+
const _KNOWN_MODES: ReadonlySet<string> = new Set([
|
|
68
|
+
'developer',
|
|
69
|
+
'reviewer',
|
|
70
|
+
'tester',
|
|
71
|
+
'po',
|
|
72
|
+
'incident',
|
|
73
|
+
'planner',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
// --- PyYAML date/timestamp parity (mirrors memory_lookup.ts) ----------------
|
|
77
|
+
|
|
78
|
+
const PYYAML_DATE_ONLY_RE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
|
79
|
+
const PYYAML_TIMESTAMP_RE =
|
|
80
|
+
/^(?:[0-9]{4}-[0-9]{2}-[0-9]{2}|[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}(?:[Tt]|[ \t]+)[0-9]{1,2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]*)?(?:[ \t]*(?:Z|[-+][0-9]{1,2}(?::[0-9]{2})?))?)$/;
|
|
81
|
+
|
|
82
|
+
/** Marker carrying PyYAML's `str(datetime.*)` form (for JSON default=str). */
|
|
83
|
+
class PyTimestamp {
|
|
84
|
+
constructor(readonly pyStr: string) {}
|
|
85
|
+
toString(): string {
|
|
86
|
+
return this.pyStr;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Marker for a Python `float` so json.dumps renders integer-valued floats as
|
|
92
|
+
* `0.0`, not `0`. JS has no int/float distinction; this preserves the
|
|
93
|
+
* `staleness_rate` (always a float via round()) byte-for-byte.
|
|
94
|
+
*/
|
|
95
|
+
class PyFloat {
|
|
96
|
+
constructor(readonly value: number) {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
100
|
+
return (
|
|
101
|
+
typeof v === 'object' &&
|
|
102
|
+
v !== null &&
|
|
103
|
+
!Array.isArray(v) &&
|
|
104
|
+
!(v instanceof PyTimestamp) &&
|
|
105
|
+
!(v instanceof PyFloat)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _isFile(p: string): boolean {
|
|
110
|
+
try {
|
|
111
|
+
return fs.statSync(p).isFile();
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _isDir(p: string): boolean {
|
|
118
|
+
try {
|
|
119
|
+
return fs.statSync(p).isDirectory();
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Mirror Python `_load_yaml`: PyYAML missing → print + exit(2). */
|
|
126
|
+
function _load_yaml(p: string): Record<string, unknown> {
|
|
127
|
+
// The `yaml` npm dependency is always present in this package; the
|
|
128
|
+
// ImportError branch (exit 2) is unreachable in the TS twin. Kept as a
|
|
129
|
+
// documented divergence candidate — Python exits 2 if PyYAML is absent.
|
|
130
|
+
const text = fs.readFileSync(p, 'utf-8');
|
|
131
|
+
const doc = parseDocument(text, { version: '1.1', prettyErrors: false });
|
|
132
|
+
if (doc.errors.length > 0) {
|
|
133
|
+
const err = doc.errors[0];
|
|
134
|
+
throw new Error(err ? err.message : 'YAML parse error');
|
|
135
|
+
}
|
|
136
|
+
YAML.visit(doc, {
|
|
137
|
+
Scalar(_key, node) {
|
|
138
|
+
if (!node.range) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const raw = text.slice(node.range[0], node.range[1]);
|
|
142
|
+
if (node.value instanceof Date) {
|
|
143
|
+
if (PYYAML_DATE_ONLY_RE.test(raw)) {
|
|
144
|
+
(node as { value: unknown }).value = new PyTimestamp(raw);
|
|
145
|
+
} else if (PYYAML_TIMESTAMP_RE.test(raw)) {
|
|
146
|
+
(node as { value: unknown }).value = new PyTimestamp(_pyYamlDatetimeStr(raw));
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (typeof node.value === 'boolean' && node.type === 'PLAIN' && /^[ynYN]$/.test(raw)) {
|
|
151
|
+
(node as { value: unknown }).value = raw;
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const data = doc.toJS({ mapAsMap: false });
|
|
156
|
+
return data == null ? {} : (data as Record<string, unknown>);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function _pad2(v: string): string {
|
|
160
|
+
return v.padStart(2, '0');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function _pyYamlDatetimeStr(raw: string): string {
|
|
164
|
+
const m =
|
|
165
|
+
/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[Tt]|[ \t]+)(\d{1,2}):(\d{2}):(\d{2})(?:\.(\d*))?(?:[ \t]*(Z|[-+]\d{1,2}(?::\d{2})?))?$/.exec(
|
|
166
|
+
raw,
|
|
167
|
+
);
|
|
168
|
+
if (!m) {
|
|
169
|
+
return raw;
|
|
170
|
+
}
|
|
171
|
+
const [, y, mo, d, h, mi, s, frac, tz] = m;
|
|
172
|
+
const date = `${y}-${_pad2(mo as string)}-${_pad2(d as string)}`;
|
|
173
|
+
let time = `${_pad2(h as string)}:${mi}:${s}`;
|
|
174
|
+
if (frac && /[1-9]/.test(frac)) {
|
|
175
|
+
time += `.${(frac + '000000').slice(0, 6)}`;
|
|
176
|
+
}
|
|
177
|
+
let off = '';
|
|
178
|
+
if (tz) {
|
|
179
|
+
if (tz === 'Z') {
|
|
180
|
+
off = '+00:00';
|
|
181
|
+
} else {
|
|
182
|
+
const tm = /^([-+])(\d{1,2})(?::(\d{2}))?$/.exec(tz);
|
|
183
|
+
if (tm) {
|
|
184
|
+
off = `${tm[1]}${_pad2(tm[2] as string)}:${tm[3] ?? '00'}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return `${date} ${time}${off}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- curated/intake iteration -----------------------------------------------
|
|
192
|
+
|
|
193
|
+
type CuratedTuple = [string, string, Record<string, unknown>];
|
|
194
|
+
|
|
195
|
+
/** Yield [file, type, entry] across both curated layouts. */
|
|
196
|
+
function _iter_curated_entries(): CuratedTuple[] {
|
|
197
|
+
const out: CuratedTuple[] = [];
|
|
198
|
+
for (const mtype of CURATED_TYPES) {
|
|
199
|
+
const single = path.join(MEMORY_ROOT, `${mtype}.yml`);
|
|
200
|
+
if (_isFile(single)) {
|
|
201
|
+
const data = _load_yaml(single);
|
|
202
|
+
const entries = data['entries'];
|
|
203
|
+
if (Array.isArray(entries)) {
|
|
204
|
+
for (const e of entries) {
|
|
205
|
+
if (_isPlainObject(e)) {
|
|
206
|
+
out.push([single, mtype, e]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const type_dir = path.join(MEMORY_ROOT, mtype);
|
|
212
|
+
if (_isDir(type_dir)) {
|
|
213
|
+
for (const yml of _rglobYml(type_dir)) {
|
|
214
|
+
const data = _load_yaml(yml) ?? {};
|
|
215
|
+
const entries = (data as Record<string, unknown>)['entries'];
|
|
216
|
+
if (Array.isArray(entries)) {
|
|
217
|
+
for (const e of entries) {
|
|
218
|
+
if (_isPlainObject(e)) {
|
|
219
|
+
out.push([yml, mtype, e]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} else if (_isPlainObject(data) && data['id']) {
|
|
223
|
+
out.push([yml, mtype, data]);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** sorted(dir.rglob("*.yml")) — recursive, lexically sorted absolute paths. */
|
|
232
|
+
function _rglobYml(root: string): string[] {
|
|
233
|
+
const found: string[] = [];
|
|
234
|
+
const walk = (dir: string): void => {
|
|
235
|
+
let entries: fs.Dirent[];
|
|
236
|
+
try {
|
|
237
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
238
|
+
} catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
for (const ent of entries) {
|
|
242
|
+
const full = path.join(dir, ent.name);
|
|
243
|
+
if (ent.isDirectory()) {
|
|
244
|
+
walk(full);
|
|
245
|
+
} else if (ent.isFile() && ent.name.endsWith('.yml')) {
|
|
246
|
+
found.push(full);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
walk(root);
|
|
251
|
+
// Python rglob ordering is unspecified but the script wraps it in
|
|
252
|
+
// sorted(); reproduce that determinism.
|
|
253
|
+
found.sort();
|
|
254
|
+
return found;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** sorted(INTAKE_ROOT.glob("*.jsonl")). */
|
|
258
|
+
function _globJsonl(root: string): string[] {
|
|
259
|
+
let names: string[];
|
|
260
|
+
try {
|
|
261
|
+
names = fs.readdirSync(root);
|
|
262
|
+
} catch {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
return names
|
|
266
|
+
.filter((n) => n.endsWith('.jsonl'))
|
|
267
|
+
.sort()
|
|
268
|
+
.map((n) => path.join(root, n));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
interface IntakeStats {
|
|
272
|
+
total_active: number;
|
|
273
|
+
superseded: number;
|
|
274
|
+
by_type: Record<string, number>;
|
|
275
|
+
by_month: Record<string, number>;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function _intake_stats(): IntakeStats {
|
|
279
|
+
let total = 0;
|
|
280
|
+
const by_type = new Map<string, number>();
|
|
281
|
+
const by_month = new Map<string, number>();
|
|
282
|
+
let superseded = 0;
|
|
283
|
+
if (_isDir(INTAKE_ROOT)) {
|
|
284
|
+
for (const jsonl of _globJsonl(INTAKE_ROOT)) {
|
|
285
|
+
const stem = path.basename(jsonl).replace(/\.jsonl$/, '');
|
|
286
|
+
const month = stem.replace('signals-', '');
|
|
287
|
+
for (const obj of _readJsonl(jsonl)) {
|
|
288
|
+
if (obj['type'] === 'supersede') {
|
|
289
|
+
superseded += 1;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
total += 1;
|
|
293
|
+
const t = obj['entry_type'];
|
|
294
|
+
if (typeof t === 'string') {
|
|
295
|
+
by_type.set(t, (by_type.get(t) ?? 0) + 1);
|
|
296
|
+
}
|
|
297
|
+
by_month.set(month, (by_month.get(month) ?? 0) + 1);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
total_active: total,
|
|
303
|
+
superseded,
|
|
304
|
+
by_type: _mapToObj(by_type),
|
|
305
|
+
by_month: _mapToObj(by_month),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Read a JSONL file, yielding parsed objects (skips blank / malformed lines). */
|
|
310
|
+
function _readJsonl(p: string): Record<string, unknown>[] {
|
|
311
|
+
let text: string;
|
|
312
|
+
try {
|
|
313
|
+
text = fs.readFileSync(p, 'utf-8');
|
|
314
|
+
} catch {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
const out: Record<string, unknown>[] = [];
|
|
318
|
+
for (const rawLine of text.split('\n')) {
|
|
319
|
+
const line = rawLine.trim();
|
|
320
|
+
if (!line) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
let obj: unknown;
|
|
324
|
+
try {
|
|
325
|
+
obj = JSON.parse(line);
|
|
326
|
+
} catch {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (_isPlainObject(obj)) {
|
|
330
|
+
out.push(obj);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Counter insertion-order → plain object (Python dict(Counter) keeps insert order). */
|
|
337
|
+
function _mapToObj(m: Map<string, number>): Record<string, number> {
|
|
338
|
+
const obj: Record<string, number> = {};
|
|
339
|
+
for (const [k, v] of m) {
|
|
340
|
+
obj[k] = v;
|
|
341
|
+
}
|
|
342
|
+
return obj;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
interface StaleRow {
|
|
346
|
+
file: string;
|
|
347
|
+
type: string;
|
|
348
|
+
id: string;
|
|
349
|
+
overdue_days: number;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Mirror dt.date.today() as a UTC-naive "local" date — uses host local date. */
|
|
353
|
+
function _todayDate(): { y: number; m: number; d: number } {
|
|
354
|
+
const now = new Date();
|
|
355
|
+
return { y: now.getFullYear(), m: now.getMonth() + 1, d: now.getDate() };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Days between two y/m/d dates (a - b). */
|
|
359
|
+
function _dateDiffDays(a: { y: number; m: number; d: number }, b: { y: number; m: number; d: number }): number {
|
|
360
|
+
const am = Date.UTC(a.y, a.m - 1, a.d);
|
|
361
|
+
const bm = Date.UTC(b.y, b.m - 1, b.d);
|
|
362
|
+
return Math.round((am - bm) / (24 * 60 * 60 * 1000));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Parse a YYYY-MM-DD date string into y/m/d, or null (mirrors date.fromisoformat). */
|
|
366
|
+
function _parseIsoDate(s: string): { y: number; m: number; d: number } | null {
|
|
367
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
368
|
+
if (!m) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const y = Number(m[1]);
|
|
372
|
+
const mo = Number(m[2]);
|
|
373
|
+
const d = Number(m[3]);
|
|
374
|
+
if (mo < 1 || mo > 12 || d < 1 || d > 31) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
return { y, m: mo, d };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function _addDays(date: { y: number; m: number; d: number }, days: number): { y: number; m: number; d: number } {
|
|
381
|
+
const ms = Date.UTC(date.y, date.m - 1, date.d) + days * 24 * 60 * 60 * 1000;
|
|
382
|
+
const dt = new Date(ms);
|
|
383
|
+
return { y: dt.getUTCFullYear(), m: dt.getUTCMonth() + 1, d: dt.getUTCDate() };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function _staleness_report(): StaleRow[] {
|
|
387
|
+
const today = _todayDate();
|
|
388
|
+
const stale: StaleRow[] = [];
|
|
389
|
+
for (const [p, mtype, entry] of _iter_curated_entries()) {
|
|
390
|
+
const lv = entry['last_validated'];
|
|
391
|
+
const review_after = entry['review_after_days'];
|
|
392
|
+
// Python: isinstance(lv, (str, dt.date)) — a PyTimestamp marks a parsed
|
|
393
|
+
// YAML date; a string is a string. review_after must be an int (not bool).
|
|
394
|
+
const lvIsDateLike = typeof lv === 'string' || lv instanceof PyTimestamp;
|
|
395
|
+
const reviewIsInt = typeof review_after === 'number' && Number.isInteger(review_after) && typeof review_after !== 'boolean';
|
|
396
|
+
if (!lvIsDateLike || !reviewIsInt) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
// Python: last = date.fromisoformat(str(lv)). str(lv) is "YYYY-MM-DD"
|
|
400
|
+
// for a date (parses) or "YYYY-MM-DD HH:MM:SS..." for a datetime/full
|
|
401
|
+
// timestamp string (fromisoformat raises ValueError → skip).
|
|
402
|
+
const lvStr = lv instanceof PyTimestamp ? lv.pyStr : (lv as string);
|
|
403
|
+
const parsed = _parseIsoDate(lvStr);
|
|
404
|
+
if (parsed === null) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const due = _addDays(parsed, review_after as number);
|
|
408
|
+
if (_dateDiffDays(due, today) < 0) {
|
|
409
|
+
stale.push({
|
|
410
|
+
file: p,
|
|
411
|
+
type: mtype,
|
|
412
|
+
id: (entry['id'] as unknown as string) ?? '?',
|
|
413
|
+
overdue_days: _dateDiffDays(today, due),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// sort by overdue_days descending, stable (mirrors Python list.sort).
|
|
418
|
+
stale.sort((a, b) => b.overdue_days - a.overdue_days);
|
|
419
|
+
return stale;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function _quarter_of(d: unknown): string {
|
|
423
|
+
let parsed: { y: number; m: number } | null = null;
|
|
424
|
+
if (typeof d === 'string') {
|
|
425
|
+
const pd = _parseIsoDate(d.slice(0, 10));
|
|
426
|
+
if (pd === null) {
|
|
427
|
+
return 'unknown';
|
|
428
|
+
}
|
|
429
|
+
parsed = pd;
|
|
430
|
+
} else if (d instanceof PyTimestamp) {
|
|
431
|
+
const pd = _parseIsoDate(d.pyStr.slice(0, 10));
|
|
432
|
+
if (pd === null) {
|
|
433
|
+
return 'unknown';
|
|
434
|
+
}
|
|
435
|
+
parsed = pd;
|
|
436
|
+
} else {
|
|
437
|
+
return 'unknown';
|
|
438
|
+
}
|
|
439
|
+
return `${parsed.y}Q${Math.floor((parsed.m - 1) / 3) + 1}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
interface QuarterlyStats {
|
|
443
|
+
accepted_by_quarter: Record<string, number>;
|
|
444
|
+
retired_by_quarter: Record<string, number>;
|
|
445
|
+
// Python `float` — preserved via PyFloat so JSON renders `0.0` not `0`.
|
|
446
|
+
staleness_rate: PyFloat;
|
|
447
|
+
curated_total: number;
|
|
448
|
+
curated_overdue: number;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function _quarterly_stats(): QuarterlyStats {
|
|
452
|
+
const accepted = new Map<string, number>();
|
|
453
|
+
for (const [, , entry] of _iter_curated_entries()) {
|
|
454
|
+
const created = entry['created'] ?? entry['last_validated'];
|
|
455
|
+
if (created !== undefined && created !== null) {
|
|
456
|
+
const q = _quarter_of(created);
|
|
457
|
+
accepted.set(q, (accepted.get(q) ?? 0) + 1);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const retired = new Map<string, number>();
|
|
461
|
+
if (_isDir(INTAKE_ROOT)) {
|
|
462
|
+
for (const jsonl of _globJsonl(INTAKE_ROOT)) {
|
|
463
|
+
for (const obj of _readJsonl(jsonl)) {
|
|
464
|
+
if (obj['type'] !== 'supersede') {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const ts = obj['ts'];
|
|
468
|
+
if (typeof ts === 'string') {
|
|
469
|
+
const q = _quarter_of(ts);
|
|
470
|
+
retired.set(q, (retired.get(q) ?? 0) + 1);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const total = _iter_curated_entries().length;
|
|
476
|
+
const overdue = _staleness_report().length;
|
|
477
|
+
const rate = total ? overdue / total : 0.0;
|
|
478
|
+
return {
|
|
479
|
+
accepted_by_quarter: _mapToObj(accepted),
|
|
480
|
+
retired_by_quarter: _mapToObj(retired),
|
|
481
|
+
staleness_rate: new PyFloat(_pyRound3(rate)),
|
|
482
|
+
curated_total: total,
|
|
483
|
+
curated_overdue: overdue,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Mirror Python round(x, 3) — banker's rounding to 3 decimals. */
|
|
488
|
+
function _pyRound3(x: number): number {
|
|
489
|
+
const factor = 1000;
|
|
490
|
+
const scaled = x * factor;
|
|
491
|
+
const floor = Math.floor(scaled);
|
|
492
|
+
const diff = scaled - floor;
|
|
493
|
+
let rounded: number;
|
|
494
|
+
if (Math.abs(diff - 0.5) < 1e-9) {
|
|
495
|
+
rounded = floor % 2 === 0 ? floor : floor + 1;
|
|
496
|
+
} else {
|
|
497
|
+
rounded = Math.round(scaled);
|
|
498
|
+
}
|
|
499
|
+
return rounded / factor;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
interface RoleModeStats {
|
|
503
|
+
total_markers: number;
|
|
504
|
+
files_scanned: number;
|
|
505
|
+
by_mode: Record<string, number>;
|
|
506
|
+
unknown_modes: string[];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function _role_mode_stats(): RoleModeStats {
|
|
510
|
+
const counts = new Map<string, number>();
|
|
511
|
+
let files_scanned = 0;
|
|
512
|
+
const unknown_modes = new Set<string>();
|
|
513
|
+
for (const root of _MODE_SCAN_DIRS) {
|
|
514
|
+
if (!_exists(root)) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
for (const md of _rglobMd(root)) {
|
|
518
|
+
let text: string;
|
|
519
|
+
try {
|
|
520
|
+
text = fs.readFileSync(md, 'utf-8');
|
|
521
|
+
} catch {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
files_scanned += 1;
|
|
525
|
+
_MODE_MARKER_PATTERN.lastIndex = 0;
|
|
526
|
+
let m: RegExpExecArray | null;
|
|
527
|
+
|
|
528
|
+
while ((m = _MODE_MARKER_PATTERN.exec(text)) !== null) {
|
|
529
|
+
const slug = (m[1] as string).toLowerCase();
|
|
530
|
+
counts.set(slug, (counts.get(slug) ?? 0) + 1);
|
|
531
|
+
if (!_KNOWN_MODES.has(slug)) {
|
|
532
|
+
unknown_modes.add(slug);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
let total = 0;
|
|
538
|
+
for (const v of counts.values()) {
|
|
539
|
+
total += v;
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
total_markers: total,
|
|
543
|
+
files_scanned,
|
|
544
|
+
by_mode: _mapToObj(counts),
|
|
545
|
+
unknown_modes: [...unknown_modes].sort(),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function _exists(p: string): boolean {
|
|
550
|
+
try {
|
|
551
|
+
fs.statSync(p);
|
|
552
|
+
return true;
|
|
553
|
+
} catch {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function _rglobMd(root: string): string[] {
|
|
559
|
+
const found: string[] = [];
|
|
560
|
+
const walk = (dir: string): void => {
|
|
561
|
+
let entries: fs.Dirent[];
|
|
562
|
+
try {
|
|
563
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
564
|
+
} catch {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
for (const ent of entries) {
|
|
568
|
+
const full = path.join(dir, ent.name);
|
|
569
|
+
if (ent.isDirectory()) {
|
|
570
|
+
walk(full);
|
|
571
|
+
} else if (ent.isFile() && ent.name.endsWith('.md')) {
|
|
572
|
+
found.push(full);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
walk(root);
|
|
577
|
+
found.sort();
|
|
578
|
+
return found;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
interface Report {
|
|
582
|
+
backend: { status: string; backend: string; reason: string };
|
|
583
|
+
intake: IntakeStats;
|
|
584
|
+
staleness: StaleRow[];
|
|
585
|
+
quarterly: QuarterlyStats;
|
|
586
|
+
role_modes: RoleModeStats;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function build_report(): Report {
|
|
590
|
+
const st = memory_status.status();
|
|
591
|
+
return {
|
|
592
|
+
backend: {
|
|
593
|
+
status: st.status,
|
|
594
|
+
backend: st.backend,
|
|
595
|
+
reason: st.reason,
|
|
596
|
+
},
|
|
597
|
+
intake: _intake_stats(),
|
|
598
|
+
staleness: _staleness_report(),
|
|
599
|
+
quarterly: _quarterly_stats(),
|
|
600
|
+
role_modes: _role_mode_stats(),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function _print_text(report: Report): void {
|
|
605
|
+
const out: string[] = [];
|
|
606
|
+
const b = report.backend;
|
|
607
|
+
out.push(`Backend: ${b.status} (backend=${b.backend})`);
|
|
608
|
+
if (b.reason) {
|
|
609
|
+
out.push(` reason: ${b.reason}`);
|
|
610
|
+
}
|
|
611
|
+
const intake = report.intake;
|
|
612
|
+
out.push(`Intake: ${intake.total_active} active, ${intake.superseded} superseded`);
|
|
613
|
+
for (const [t, n] of _sortedEntries(intake.by_type)) {
|
|
614
|
+
out.push(` - ${t}: ${n}`);
|
|
615
|
+
}
|
|
616
|
+
const stale = report.staleness;
|
|
617
|
+
if (stale.length === 0) {
|
|
618
|
+
out.push('Staleness: no curated entries past review_after_days');
|
|
619
|
+
} else {
|
|
620
|
+
out.push(`Staleness: ${stale.length} entrie(s) overdue`);
|
|
621
|
+
for (const row of stale.slice(0, 5)) {
|
|
622
|
+
out.push(` - ${row.id} (${row.type}) +${row.overdue_days}d ${row.file}`);
|
|
623
|
+
}
|
|
624
|
+
if (stale.length > 5) {
|
|
625
|
+
out.push(` (+${stale.length - 5} more)`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const q = report.quarterly;
|
|
629
|
+
out.push(`Quarterly: staleness-rate=${_pyPercent1(q.staleness_rate.value)} (${q.curated_overdue}/${q.curated_total})`);
|
|
630
|
+
if (Object.keys(q.accepted_by_quarter).length > 0) {
|
|
631
|
+
const acc = _sortedEntries(q.accepted_by_quarter)
|
|
632
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
633
|
+
.join(', ');
|
|
634
|
+
out.push(` accepted: ${acc}`);
|
|
635
|
+
}
|
|
636
|
+
if (Object.keys(q.retired_by_quarter).length > 0) {
|
|
637
|
+
const ret = _sortedEntries(q.retired_by_quarter)
|
|
638
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
639
|
+
.join(', ');
|
|
640
|
+
out.push(` retired: ${ret}`);
|
|
641
|
+
}
|
|
642
|
+
const rm = report.role_modes;
|
|
643
|
+
if (rm.files_scanned) {
|
|
644
|
+
out.push(`Role modes: ${rm.total_markers} marker(s) in ${rm.files_scanned} file(s)`);
|
|
645
|
+
if (Object.keys(rm.by_mode).length > 0) {
|
|
646
|
+
const modes = _sortedEntries(rm.by_mode)
|
|
647
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
648
|
+
.join(', ');
|
|
649
|
+
out.push(` by mode: ${modes}`);
|
|
650
|
+
}
|
|
651
|
+
if (rm.unknown_modes.length > 0) {
|
|
652
|
+
out.push(` unknown: ${rm.unknown_modes.join(', ')} (not in the six reserved slugs)`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
process.stdout.write(out.map((line) => `${line}\n`).join(''));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** sorted(dict.items()) — by key, lexically. */
|
|
659
|
+
function _sortedEntries(obj: Record<string, number>): [string, number][] {
|
|
660
|
+
return Object.entries(obj).sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Mirror Python f"{x:.1%}" — percentage with one decimal. */
|
|
664
|
+
function _pyPercent1(x: number): string {
|
|
665
|
+
return `${(x * 100).toFixed(1)}%`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// --- JSON output (json.dumps(report, indent=2, default=str)) ----------------
|
|
669
|
+
|
|
670
|
+
function pyJsonDumps(value: unknown, indent: number): string {
|
|
671
|
+
return _escapeNonAscii(_dumpsIndent(value, indent, 0));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function _dumpsIndent(value: unknown, indent: number, depth: number): string {
|
|
675
|
+
const pad = ' '.repeat(indent * (depth + 1));
|
|
676
|
+
const closePad = ' '.repeat(indent * depth);
|
|
677
|
+
if (value === null || value === undefined) {
|
|
678
|
+
return 'null';
|
|
679
|
+
}
|
|
680
|
+
if (typeof value === 'boolean') {
|
|
681
|
+
return value ? 'true' : 'false';
|
|
682
|
+
}
|
|
683
|
+
if (typeof value === 'number') {
|
|
684
|
+
return _jsonNum(value);
|
|
685
|
+
}
|
|
686
|
+
if (typeof value === 'string') {
|
|
687
|
+
return _jsonStrAscii(value);
|
|
688
|
+
}
|
|
689
|
+
if (value instanceof PyTimestamp) {
|
|
690
|
+
return _jsonStrAscii(value.pyStr);
|
|
691
|
+
}
|
|
692
|
+
if (value instanceof PyFloat) {
|
|
693
|
+
return _jsonFloat(value.value);
|
|
694
|
+
}
|
|
695
|
+
if (Array.isArray(value)) {
|
|
696
|
+
if (value.length === 0) {
|
|
697
|
+
return '[]';
|
|
698
|
+
}
|
|
699
|
+
const items = value.map((v) => pad + _dumpsIndent(v, indent, depth + 1));
|
|
700
|
+
return `[\n${items.join(',\n')}\n${closePad}]`;
|
|
701
|
+
}
|
|
702
|
+
if (_isPlainObject(value)) {
|
|
703
|
+
const keys = Object.keys(value);
|
|
704
|
+
if (keys.length === 0) {
|
|
705
|
+
return '{}';
|
|
706
|
+
}
|
|
707
|
+
const items = keys.map((k) => `${pad}${_jsonStrAscii(k)}: ${_dumpsIndent(value[k], indent, depth + 1)}`);
|
|
708
|
+
return `{\n${items.join(',\n')}\n${closePad}}`;
|
|
709
|
+
}
|
|
710
|
+
return _jsonStrAscii(String(value));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function _jsonNum(n: number): string {
|
|
714
|
+
if (!Number.isFinite(n)) {
|
|
715
|
+
if (Number.isNaN(n)) {
|
|
716
|
+
return 'NaN';
|
|
717
|
+
}
|
|
718
|
+
return n > 0 ? 'Infinity' : '-Infinity';
|
|
719
|
+
}
|
|
720
|
+
return String(n);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Render a Python float — integer-valued floats keep a `.0` suffix (repr(float)). */
|
|
724
|
+
function _jsonFloat(n: number): string {
|
|
725
|
+
if (!Number.isFinite(n)) {
|
|
726
|
+
if (Number.isNaN(n)) {
|
|
727
|
+
return 'NaN';
|
|
728
|
+
}
|
|
729
|
+
return n > 0 ? 'Infinity' : '-Infinity';
|
|
730
|
+
}
|
|
731
|
+
return Number.isInteger(n) ? `${n}.0` : String(n);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function _jsonStrAscii(s: string): string {
|
|
735
|
+
let out = '"';
|
|
736
|
+
for (const ch of s) {
|
|
737
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
738
|
+
switch (ch) {
|
|
739
|
+
case '"':
|
|
740
|
+
out += '\\"';
|
|
741
|
+
break;
|
|
742
|
+
case '\\':
|
|
743
|
+
out += '\\\\';
|
|
744
|
+
break;
|
|
745
|
+
case '\n':
|
|
746
|
+
out += '\\n';
|
|
747
|
+
break;
|
|
748
|
+
case '\r':
|
|
749
|
+
out += '\\r';
|
|
750
|
+
break;
|
|
751
|
+
case '\t':
|
|
752
|
+
out += '\\t';
|
|
753
|
+
break;
|
|
754
|
+
case '\b':
|
|
755
|
+
out += '\\b';
|
|
756
|
+
break;
|
|
757
|
+
case '\f':
|
|
758
|
+
out += '\\f';
|
|
759
|
+
break;
|
|
760
|
+
default:
|
|
761
|
+
if (code < 0x20) {
|
|
762
|
+
out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
763
|
+
} else {
|
|
764
|
+
out += ch;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return `${out}"`;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function _escapeNonAscii(s: string): string {
|
|
772
|
+
let out = '';
|
|
773
|
+
for (const ch of s) {
|
|
774
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
775
|
+
if (code > 0x7f) {
|
|
776
|
+
for (let i = 0; i < ch.length; i += 1) {
|
|
777
|
+
out += `\\u${ch.charCodeAt(i).toString(16).padStart(4, '0')}`;
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
out += ch;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
interface ParsedArgs {
|
|
787
|
+
format: 'text' | 'json';
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const _PROG = 'memory_report.py';
|
|
791
|
+
const _USAGE = 'usage: memory_report.py [-h] [--format {text,json}]\n';
|
|
792
|
+
|
|
793
|
+
/** Mirror argparse error: print usage + "<prog>: error: <msg>" to stderr, exit 2. */
|
|
794
|
+
function _argError(msg: string): never {
|
|
795
|
+
process.stderr.write(_USAGE);
|
|
796
|
+
process.stderr.write(`${_PROG}: error: ${msg}\n`);
|
|
797
|
+
process.exit(2);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function _parseArgs(argv: string[]): ParsedArgs {
|
|
801
|
+
const args: ParsedArgs = { format: 'text' };
|
|
802
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
803
|
+
const a = argv[i] as string;
|
|
804
|
+
if (a === '--format') {
|
|
805
|
+
args.format = _checkChoice(argv[++i], ['text', 'json'], '--format') as 'text' | 'json';
|
|
806
|
+
} else if (a.startsWith('--format=')) {
|
|
807
|
+
args.format = _checkChoice(a.slice('--format='.length), ['text', 'json'], '--format') as
|
|
808
|
+
| 'text'
|
|
809
|
+
| 'json';
|
|
810
|
+
} else if (a === '-h' || a === '--help') {
|
|
811
|
+
process.stdout.write(_USAGE);
|
|
812
|
+
process.exit(0);
|
|
813
|
+
} else {
|
|
814
|
+
_argError(`unrecognized arguments: ${a}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return args;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function _checkChoice(value: string | undefined, choices: string[], flag: string): string {
|
|
821
|
+
if (value === undefined || !choices.includes(value)) {
|
|
822
|
+
_argError(
|
|
823
|
+
`argument ${flag}: invalid choice: '${value ?? ''}' (choose from ${choices
|
|
824
|
+
.map((c) => `'${c}'`)
|
|
825
|
+
.join(', ')})`,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
return value;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export function main(): number {
|
|
832
|
+
const args = _parseArgs(process.argv.slice(2));
|
|
833
|
+
const report = build_report();
|
|
834
|
+
if (args.format === 'json') {
|
|
835
|
+
process.stdout.write(`${pyJsonDumps(report, 2)}\n`);
|
|
836
|
+
} else {
|
|
837
|
+
_print_text(report);
|
|
838
|
+
}
|
|
839
|
+
return 0;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function _isCliEntry(): boolean {
|
|
843
|
+
if (process.argv[1] === undefined) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
847
|
+
if (import.meta.url === argvUrl) {
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
// A symlinked invocation (e.g. via an installed `.augment/` projection,
|
|
851
|
+
// or macOS /var → /private/var temp dirs) makes the raw URLs differ:
|
|
852
|
+
// import.meta.url is the resolved real path while argv[1] keeps the
|
|
853
|
+
// symlink path. Compare realpaths so the entry guard still fires
|
|
854
|
+
// (without this the CLI silently no-ops when run through a symlink).
|
|
855
|
+
try {
|
|
856
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
857
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
858
|
+
return here === argv;
|
|
859
|
+
} catch {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const _isMain =
|
|
865
|
+
_isCliEntry();
|
|
866
|
+
if (_isMain) {
|
|
867
|
+
process.exit(main());
|
|
868
|
+
}
|