Compare commits
17 Commits
main
...
demo-lates
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5225211e45 | ||
|
|
4f0b6fb9b3 | ||
|
|
8e781ac325 | ||
|
|
ee0450b7c3 | ||
|
|
0f9b4aa2fe | ||
|
|
e4eb0fa168 | ||
|
|
93081bed5c | ||
|
|
885a1acf19 | ||
|
|
97888e3775 | ||
|
|
9b8befaa94 | ||
|
|
2965b68a9d | ||
|
|
08c8debfff | ||
|
|
b02cf5b9e1 | ||
|
|
1ac8045924 | ||
|
|
52d2da2f44 | ||
|
|
d44ad3af1e | ||
|
|
89ac6a9b5b |
4
.gitignore
vendored
4
.gitignore
vendored
@ -4,3 +4,7 @@
|
|||||||
.env
|
.env
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
|
# Headshot pool — binary face JPGs are fetched by scripts/staffing/fetch_face_pool.py
|
||||||
|
# (synthetic StyleGAN, ~100MB for 200 faces). Manifest + fetch script are tracked.
|
||||||
|
data/headshots/face_*.jpg
|
||||||
|
|||||||
@ -90,7 +90,7 @@ pub struct IterateFailure {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn iterate(
|
pub async fn iterate(
|
||||||
State(state): State<super::V1State>,
|
State(_state): State<super::V1State>,
|
||||||
Json(req): Json<IterateRequest>,
|
Json(req): Json<IterateRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let max_iter = req.max_iterations.unwrap_or(DEFAULT_MAX_ITERATIONS).max(1);
|
let max_iter = req.max_iterations.unwrap_or(DEFAULT_MAX_ITERATIONS).max(1);
|
||||||
|
|||||||
@ -11,15 +11,51 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"created_at": "2026-04-20T11:07:57.308050648Z",
|
"created_at": "2026-04-20T11:07:57.308050648Z",
|
||||||
"updated_at": "2026-04-22T03:28:28.343843823Z",
|
"updated_at": "2026-04-28T01:28:31.280305207Z",
|
||||||
"description": "",
|
"description": "",
|
||||||
"owner": "",
|
"owner": "",
|
||||||
"sensitivity": null,
|
"sensitivity": null,
|
||||||
"columns": [],
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "timestamp",
|
||||||
|
"data_type": "Utf8",
|
||||||
|
"sensitivity": null,
|
||||||
|
"description": "",
|
||||||
|
"is_pii": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "operation",
|
||||||
|
"data_type": "Utf8",
|
||||||
|
"sensitivity": null,
|
||||||
|
"description": "",
|
||||||
|
"is_pii": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "approach",
|
||||||
|
"data_type": "Utf8",
|
||||||
|
"sensitivity": null,
|
||||||
|
"description": "",
|
||||||
|
"is_pii": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "result",
|
||||||
|
"data_type": "Utf8",
|
||||||
|
"sensitivity": null,
|
||||||
|
"description": "",
|
||||||
|
"is_pii": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "context",
|
||||||
|
"data_type": "Utf8",
|
||||||
|
"sensitivity": null,
|
||||||
|
"description": "",
|
||||||
|
"is_pii": false
|
||||||
|
}
|
||||||
|
],
|
||||||
"lineage": null,
|
"lineage": null,
|
||||||
"freshness": null,
|
"freshness": null,
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"row_count": null,
|
"row_count": 2077,
|
||||||
"last_embedded_at": null,
|
"last_embedded_at": null,
|
||||||
"embedding_stale_since": null,
|
"embedding_stale_since": null,
|
||||||
"embedding_refresh_policy": null
|
"embedding_refresh_policy": null
|
||||||
|
|||||||
@ -1,117 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "564b00ae-cbf3-4efd-aa55-84cdb6d2b0b7",
|
|
||||||
"name": "client_workerskjkk",
|
|
||||||
"schema_fingerprint": "cdfe85348885ddf329e5e6e9bf0e2c75c92d1a86fdb0fd3875ed46e3f93c4d82",
|
|
||||||
"objects": [
|
|
||||||
{
|
|
||||||
"bucket": "primary",
|
|
||||||
"key": "datasets/client_workerskjkk.parquet",
|
|
||||||
"size_bytes": 32201,
|
|
||||||
"created_at": "2026-04-21T00:49:04.623625149Z"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"created_at": "2026-04-21T00:49:04.623626738Z",
|
|
||||||
"updated_at": "2026-04-21T00:49:04.623901788Z",
|
|
||||||
"description": "",
|
|
||||||
"owner": "",
|
|
||||||
"sensitivity": "pii",
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "worker_id",
|
|
||||||
"data_type": "Utf8",
|
|
||||||
"sensitivity": null,
|
|
||||||
"description": "",
|
|
||||||
"is_pii": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"data_type": "Utf8",
|
|
||||||
"sensitivity": "pii",
|
|
||||||
"description": "",
|
|
||||||
"is_pii": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "role",
|
|
||||||
"data_type": "Utf8",
|
|
||||||
"sensitivity": null,
|
|
||||||
"description": "",
|
|
||||||
"is_pii": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "city",
|
|
||||||
"data_type": "Utf8",
|
|
||||||
"sensitivity": null,
|
|
||||||
"description": "",
|
|
||||||
"is_pii": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "state",
|
|
||||||
"data_type": "Utf8",
|
|
||||||
"sensitivity": null,
|
|
||||||
"description": "",
|
|
||||||
"is_pii": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "email",
|
|
||||||
"data_type": "Utf8",
|
|
||||||
"sensitivity": "pii",
|
|
||||||
"description": "",
|
|
||||||
"is_pii": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "phone",
|
|
||||||
"data_type": "Utf8",
|
|
||||||
"sensitivity": "pii",
|
|
||||||
"description": "",
|
|
||||||
"is_pii": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "skills",
|
|
||||||
"data_type": "Utf8",
|
|
||||||
"sensitivity": null,
|
|
||||||
"description": "",
|
|
||||||
"is_pii": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "certifications",
|
|
||||||
"data_type": "Utf8",
|
|
||||||
"sensitivity": null,
|
|
||||||
"description": "",
|
|
||||||
"is_pii": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "availability",
|
|
||||||
"data_type": "Float64",
|
|
||||||
"sensitivity": null,
|
|
||||||
"description": "",
|
|
||||||
"is_pii": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "reliability",
|
|
||||||
"data_type": "Float64",
|
|
||||||
"sensitivity": null,
|
|
||||||
"description": "",
|
|
||||||
"is_pii": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "archetype",
|
|
||||||
"data_type": "Utf8",
|
|
||||||
"sensitivity": null,
|
|
||||||
"description": "",
|
|
||||||
"is_pii": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"lineage": {
|
|
||||||
"source_system": "csv",
|
|
||||||
"source_file": "staffing_roster_sample.csv",
|
|
||||||
"ingest_job": "ingest-1776732544623",
|
|
||||||
"ingest_timestamp": "2026-04-21T00:49:04.623625149Z",
|
|
||||||
"parent_datasets": []
|
|
||||||
},
|
|
||||||
"freshness": null,
|
|
||||||
"tags": [],
|
|
||||||
"row_count": 180,
|
|
||||||
"last_embedded_at": null,
|
|
||||||
"embedding_stale_since": null,
|
|
||||||
"embedding_refresh_policy": null
|
|
||||||
}
|
|
||||||
198
data/headshots/manifest.jsonl
Normal file
198
data/headshots/manifest.jsonl
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
{"id": 0, "file": "face_0000.jpg", "gender": null}
|
||||||
|
{"id": 1, "file": "face_0001.jpg", "gender": null}
|
||||||
|
{"id": 2, "file": "face_0002.jpg", "gender": null}
|
||||||
|
{"id": 3, "file": "face_0003.jpg", "gender": null}
|
||||||
|
{"id": 4, "file": "face_0004.jpg", "gender": null}
|
||||||
|
{"id": 5, "file": "face_0005.jpg", "gender": null}
|
||||||
|
{"id": 6, "file": "face_0006.jpg", "gender": null}
|
||||||
|
{"id": 7, "file": "face_0007.jpg", "gender": null}
|
||||||
|
{"id": 8, "file": "face_0008.jpg", "gender": null}
|
||||||
|
{"id": 9, "file": "face_0009.jpg", "gender": null}
|
||||||
|
{"id": 10, "file": "face_0010.jpg", "gender": null}
|
||||||
|
{"id": 11, "file": "face_0011.jpg", "gender": null}
|
||||||
|
{"id": 12, "file": "face_0012.jpg", "gender": null}
|
||||||
|
{"id": 13, "file": "face_0013.jpg", "gender": null}
|
||||||
|
{"id": 14, "file": "face_0014.jpg", "gender": null}
|
||||||
|
{"id": 15, "file": "face_0015.jpg", "gender": null}
|
||||||
|
{"id": 16, "file": "face_0016.jpg", "gender": null}
|
||||||
|
{"id": 17, "file": "face_0017.jpg", "gender": null}
|
||||||
|
{"id": 18, "file": "face_0018.jpg", "gender": null}
|
||||||
|
{"id": 19, "file": "face_0019.jpg", "gender": null}
|
||||||
|
{"id": 20, "file": "face_0020.jpg", "gender": null}
|
||||||
|
{"id": 21, "file": "face_0021.jpg", "gender": null}
|
||||||
|
{"id": 22, "file": "face_0022.jpg", "gender": null}
|
||||||
|
{"id": 23, "file": "face_0023.jpg", "gender": null}
|
||||||
|
{"id": 24, "file": "face_0024.jpg", "gender": null}
|
||||||
|
{"id": 25, "file": "face_0025.jpg", "gender": null}
|
||||||
|
{"id": 26, "file": "face_0026.jpg", "gender": null}
|
||||||
|
{"id": 27, "file": "face_0027.jpg", "gender": null}
|
||||||
|
{"id": 28, "file": "face_0028.jpg", "gender": null}
|
||||||
|
{"id": 29, "file": "face_0029.jpg", "gender": null}
|
||||||
|
{"id": 30, "file": "face_0030.jpg", "gender": null}
|
||||||
|
{"id": 31, "file": "face_0031.jpg", "gender": null}
|
||||||
|
{"id": 32, "file": "face_0032.jpg", "gender": null}
|
||||||
|
{"id": 33, "file": "face_0033.jpg", "gender": null}
|
||||||
|
{"id": 34, "file": "face_0034.jpg", "gender": null}
|
||||||
|
{"id": 35, "file": "face_0035.jpg", "gender": null}
|
||||||
|
{"id": 36, "file": "face_0036.jpg", "gender": null}
|
||||||
|
{"id": 37, "file": "face_0037.jpg", "gender": null}
|
||||||
|
{"id": 38, "file": "face_0038.jpg", "gender": null}
|
||||||
|
{"id": 39, "file": "face_0039.jpg", "gender": null}
|
||||||
|
{"id": 40, "file": "face_0040.jpg", "gender": null}
|
||||||
|
{"id": 41, "file": "face_0041.jpg", "gender": null}
|
||||||
|
{"id": 42, "file": "face_0042.jpg", "gender": null}
|
||||||
|
{"id": 43, "file": "face_0043.jpg", "gender": null}
|
||||||
|
{"id": 44, "file": "face_0044.jpg", "gender": null}
|
||||||
|
{"id": 45, "file": "face_0045.jpg", "gender": null}
|
||||||
|
{"id": 46, "file": "face_0046.jpg", "gender": null}
|
||||||
|
{"id": 47, "file": "face_0047.jpg", "gender": null}
|
||||||
|
{"id": 48, "file": "face_0048.jpg", "gender": null}
|
||||||
|
{"id": 49, "file": "face_0049.jpg", "gender": null}
|
||||||
|
{"id": 50, "file": "face_0050.jpg", "gender": null}
|
||||||
|
{"id": 51, "file": "face_0051.jpg", "gender": null}
|
||||||
|
{"id": 52, "file": "face_0052.jpg", "gender": null}
|
||||||
|
{"id": 53, "file": "face_0053.jpg", "gender": null}
|
||||||
|
{"id": 54, "file": "face_0054.jpg", "gender": null}
|
||||||
|
{"id": 55, "file": "face_0055.jpg", "gender": null}
|
||||||
|
{"id": 56, "file": "face_0056.jpg", "gender": null}
|
||||||
|
{"id": 57, "file": "face_0057.jpg", "gender": null}
|
||||||
|
{"id": 58, "file": "face_0058.jpg", "gender": null}
|
||||||
|
{"id": 59, "file": "face_0059.jpg", "gender": null}
|
||||||
|
{"id": 60, "file": "face_0060.jpg", "gender": null}
|
||||||
|
{"id": 61, "file": "face_0061.jpg", "gender": null}
|
||||||
|
{"id": 62, "file": "face_0062.jpg", "gender": null}
|
||||||
|
{"id": 63, "file": "face_0063.jpg", "gender": null}
|
||||||
|
{"id": 64, "file": "face_0064.jpg", "gender": null}
|
||||||
|
{"id": 65, "file": "face_0065.jpg", "gender": null}
|
||||||
|
{"id": 66, "file": "face_0066.jpg", "gender": null}
|
||||||
|
{"id": 67, "file": "face_0067.jpg", "gender": null}
|
||||||
|
{"id": 68, "file": "face_0068.jpg", "gender": null}
|
||||||
|
{"id": 69, "file": "face_0069.jpg", "gender": null}
|
||||||
|
{"id": 70, "file": "face_0070.jpg", "gender": null}
|
||||||
|
{"id": 71, "file": "face_0071.jpg", "gender": null}
|
||||||
|
{"id": 72, "file": "face_0072.jpg", "gender": null}
|
||||||
|
{"id": 73, "file": "face_0073.jpg", "gender": null}
|
||||||
|
{"id": 74, "file": "face_0074.jpg", "gender": null}
|
||||||
|
{"id": 75, "file": "face_0075.jpg", "gender": null}
|
||||||
|
{"id": 76, "file": "face_0076.jpg", "gender": null}
|
||||||
|
{"id": 77, "file": "face_0077.jpg", "gender": null}
|
||||||
|
{"id": 78, "file": "face_0078.jpg", "gender": null}
|
||||||
|
{"id": 79, "file": "face_0079.jpg", "gender": null}
|
||||||
|
{"id": 80, "file": "face_0080.jpg", "gender": null}
|
||||||
|
{"id": 81, "file": "face_0081.jpg", "gender": null}
|
||||||
|
{"id": 82, "file": "face_0082.jpg", "gender": null}
|
||||||
|
{"id": 83, "file": "face_0083.jpg", "gender": null}
|
||||||
|
{"id": 84, "file": "face_0084.jpg", "gender": null}
|
||||||
|
{"id": 85, "file": "face_0085.jpg", "gender": null}
|
||||||
|
{"id": 86, "file": "face_0086.jpg", "gender": null}
|
||||||
|
{"id": 87, "file": "face_0087.jpg", "gender": null}
|
||||||
|
{"id": 88, "file": "face_0088.jpg", "gender": null}
|
||||||
|
{"id": 89, "file": "face_0089.jpg", "gender": null}
|
||||||
|
{"id": 90, "file": "face_0090.jpg", "gender": null}
|
||||||
|
{"id": 91, "file": "face_0091.jpg", "gender": null}
|
||||||
|
{"id": 92, "file": "face_0092.jpg", "gender": null}
|
||||||
|
{"id": 93, "file": "face_0093.jpg", "gender": null}
|
||||||
|
{"id": 94, "file": "face_0094.jpg", "gender": null}
|
||||||
|
{"id": 95, "file": "face_0095.jpg", "gender": null}
|
||||||
|
{"id": 96, "file": "face_0096.jpg", "gender": null}
|
||||||
|
{"id": 97, "file": "face_0097.jpg", "gender": null}
|
||||||
|
{"id": 98, "file": "face_0098.jpg", "gender": null}
|
||||||
|
{"id": 99, "file": "face_0099.jpg", "gender": null}
|
||||||
|
{"id": 100, "file": "face_0100.jpg", "gender": null}
|
||||||
|
{"id": 101, "file": "face_0101.jpg", "gender": null}
|
||||||
|
{"id": 102, "file": "face_0102.jpg", "gender": null}
|
||||||
|
{"id": 103, "file": "face_0103.jpg", "gender": null}
|
||||||
|
{"id": 104, "file": "face_0104.jpg", "gender": null}
|
||||||
|
{"id": 105, "file": "face_0105.jpg", "gender": null}
|
||||||
|
{"id": 106, "file": "face_0106.jpg", "gender": null}
|
||||||
|
{"id": 107, "file": "face_0107.jpg", "gender": null}
|
||||||
|
{"id": 108, "file": "face_0108.jpg", "gender": null}
|
||||||
|
{"id": 109, "file": "face_0109.jpg", "gender": null}
|
||||||
|
{"id": 110, "file": "face_0110.jpg", "gender": null}
|
||||||
|
{"id": 111, "file": "face_0111.jpg", "gender": null}
|
||||||
|
{"id": 112, "file": "face_0112.jpg", "gender": null}
|
||||||
|
{"id": 113, "file": "face_0113.jpg", "gender": null}
|
||||||
|
{"id": 114, "file": "face_0114.jpg", "gender": null}
|
||||||
|
{"id": 115, "file": "face_0115.jpg", "gender": null}
|
||||||
|
{"id": 116, "file": "face_0116.jpg", "gender": null}
|
||||||
|
{"id": 117, "file": "face_0117.jpg", "gender": null}
|
||||||
|
{"id": 118, "file": "face_0118.jpg", "gender": null}
|
||||||
|
{"id": 119, "file": "face_0119.jpg", "gender": null}
|
||||||
|
{"id": 121, "file": "face_0121.jpg", "gender": null}
|
||||||
|
{"id": 122, "file": "face_0122.jpg", "gender": null}
|
||||||
|
{"id": 123, "file": "face_0123.jpg", "gender": null}
|
||||||
|
{"id": 124, "file": "face_0124.jpg", "gender": null}
|
||||||
|
{"id": 125, "file": "face_0125.jpg", "gender": null}
|
||||||
|
{"id": 126, "file": "face_0126.jpg", "gender": null}
|
||||||
|
{"id": 127, "file": "face_0127.jpg", "gender": null}
|
||||||
|
{"id": 128, "file": "face_0128.jpg", "gender": null}
|
||||||
|
{"id": 129, "file": "face_0129.jpg", "gender": null}
|
||||||
|
{"id": 130, "file": "face_0130.jpg", "gender": null}
|
||||||
|
{"id": 131, "file": "face_0131.jpg", "gender": null}
|
||||||
|
{"id": 132, "file": "face_0132.jpg", "gender": null}
|
||||||
|
{"id": 133, "file": "face_0133.jpg", "gender": null}
|
||||||
|
{"id": 134, "file": "face_0134.jpg", "gender": null}
|
||||||
|
{"id": 135, "file": "face_0135.jpg", "gender": null}
|
||||||
|
{"id": 136, "file": "face_0136.jpg", "gender": null}
|
||||||
|
{"id": 137, "file": "face_0137.jpg", "gender": null}
|
||||||
|
{"id": 138, "file": "face_0138.jpg", "gender": null}
|
||||||
|
{"id": 139, "file": "face_0139.jpg", "gender": null}
|
||||||
|
{"id": 140, "file": "face_0140.jpg", "gender": null}
|
||||||
|
{"id": 141, "file": "face_0141.jpg", "gender": null}
|
||||||
|
{"id": 142, "file": "face_0142.jpg", "gender": null}
|
||||||
|
{"id": 143, "file": "face_0143.jpg", "gender": null}
|
||||||
|
{"id": 144, "file": "face_0144.jpg", "gender": null}
|
||||||
|
{"id": 145, "file": "face_0145.jpg", "gender": null}
|
||||||
|
{"id": 146, "file": "face_0146.jpg", "gender": null}
|
||||||
|
{"id": 147, "file": "face_0147.jpg", "gender": null}
|
||||||
|
{"id": 148, "file": "face_0148.jpg", "gender": null}
|
||||||
|
{"id": 149, "file": "face_0149.jpg", "gender": null}
|
||||||
|
{"id": 150, "file": "face_0150.jpg", "gender": null}
|
||||||
|
{"id": 151, "file": "face_0151.jpg", "gender": null}
|
||||||
|
{"id": 152, "file": "face_0152.jpg", "gender": null}
|
||||||
|
{"id": 153, "file": "face_0153.jpg", "gender": null}
|
||||||
|
{"id": 154, "file": "face_0154.jpg", "gender": null}
|
||||||
|
{"id": 155, "file": "face_0155.jpg", "gender": null}
|
||||||
|
{"id": 156, "file": "face_0156.jpg", "gender": null}
|
||||||
|
{"id": 157, "file": "face_0157.jpg", "gender": null}
|
||||||
|
{"id": 158, "file": "face_0158.jpg", "gender": null}
|
||||||
|
{"id": 159, "file": "face_0159.jpg", "gender": null}
|
||||||
|
{"id": 160, "file": "face_0160.jpg", "gender": null}
|
||||||
|
{"id": 161, "file": "face_0161.jpg", "gender": null}
|
||||||
|
{"id": 162, "file": "face_0162.jpg", "gender": null}
|
||||||
|
{"id": 163, "file": "face_0163.jpg", "gender": null}
|
||||||
|
{"id": 164, "file": "face_0164.jpg", "gender": null}
|
||||||
|
{"id": 165, "file": "face_0165.jpg", "gender": null}
|
||||||
|
{"id": 166, "file": "face_0166.jpg", "gender": null}
|
||||||
|
{"id": 167, "file": "face_0167.jpg", "gender": null}
|
||||||
|
{"id": 168, "file": "face_0168.jpg", "gender": null}
|
||||||
|
{"id": 169, "file": "face_0169.jpg", "gender": null}
|
||||||
|
{"id": 170, "file": "face_0170.jpg", "gender": null}
|
||||||
|
{"id": 171, "file": "face_0171.jpg", "gender": null}
|
||||||
|
{"id": 172, "file": "face_0172.jpg", "gender": null}
|
||||||
|
{"id": 173, "file": "face_0173.jpg", "gender": null}
|
||||||
|
{"id": 174, "file": "face_0174.jpg", "gender": null}
|
||||||
|
{"id": 175, "file": "face_0175.jpg", "gender": null}
|
||||||
|
{"id": 176, "file": "face_0176.jpg", "gender": null}
|
||||||
|
{"id": 177, "file": "face_0177.jpg", "gender": null}
|
||||||
|
{"id": 178, "file": "face_0178.jpg", "gender": null}
|
||||||
|
{"id": 179, "file": "face_0179.jpg", "gender": null}
|
||||||
|
{"id": 180, "file": "face_0180.jpg", "gender": null}
|
||||||
|
{"id": 181, "file": "face_0181.jpg", "gender": null}
|
||||||
|
{"id": 182, "file": "face_0182.jpg", "gender": null}
|
||||||
|
{"id": 183, "file": "face_0183.jpg", "gender": null}
|
||||||
|
{"id": 184, "file": "face_0184.jpg", "gender": null}
|
||||||
|
{"id": 185, "file": "face_0185.jpg", "gender": null}
|
||||||
|
{"id": 186, "file": "face_0186.jpg", "gender": null}
|
||||||
|
{"id": 187, "file": "face_0187.jpg", "gender": null}
|
||||||
|
{"id": 188, "file": "face_0188.jpg", "gender": null}
|
||||||
|
{"id": 189, "file": "face_0189.jpg", "gender": null}
|
||||||
|
{"id": 191, "file": "face_0191.jpg", "gender": null}
|
||||||
|
{"id": 192, "file": "face_0192.jpg", "gender": null}
|
||||||
|
{"id": 193, "file": "face_0193.jpg", "gender": null}
|
||||||
|
{"id": 194, "file": "face_0194.jpg", "gender": null}
|
||||||
|
{"id": 195, "file": "face_0195.jpg", "gender": null}
|
||||||
|
{"id": 196, "file": "face_0196.jpg", "gender": null}
|
||||||
|
{"id": 197, "file": "face_0197.jpg", "gender": null}
|
||||||
|
{"id": 198, "file": "face_0198.jpg", "gender": null}
|
||||||
|
{"id": 199, "file": "face_0199.jpg", "gender": null}
|
||||||
@ -51,9 +51,23 @@ details .body{padding-top:10px;font-size:12px;color:#8b949e}
|
|||||||
.accent-b{border-left:3px solid #1f6feb}
|
.accent-b{border-left:3px solid #1f6feb}
|
||||||
.accent-a{border-left:3px solid #bc8cff}
|
.accent-a{border-left:3px solid #bc8cff}
|
||||||
.accent-w{border-left:3px solid #d29922}
|
.accent-w{border-left:3px solid #d29922}
|
||||||
|
.accent-g{border-left:3px solid #3fb950}
|
||||||
|
.accent-r{border-left:3px solid #f85149}
|
||||||
|
|
||||||
.worker{display:flex;align-items:center;gap:10px;padding:8px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;font-size:12px}
|
.worker{display:flex;align-items:center;gap:10px;padding:8px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;font-size:12px;border-left:3px solid #30363d}
|
||||||
.worker .av{width:28px;height:28px;border-radius:6px;background:#1a2744;display:flex;align-items:center;justify-content:center;font-weight:600;color:#e6edf3;font-size:10px;flex-shrink:0}
|
.worker .av{width:32px;height:32px;border-radius:50%;background:#0d1117;border:1px solid #21262d;display:flex;align-items:center;justify-content:center;font-weight:600;color:#c9d1d9;font-size:11px;flex-shrink:0;letter-spacing:0.5px;overflow:hidden;position:relative}
|
||||||
|
.worker .av img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
||||||
|
.worker[data-role-band="warehouse"]{border-left-color:#58a6ff}
|
||||||
|
.worker[data-role-band="production"]{border-left-color:#d29922}
|
||||||
|
.worker[data-role-band="trades"]{border-left-color:#bc8cff}
|
||||||
|
.worker[data-role-band="driver"]{border-left-color:#3fb950}
|
||||||
|
.worker[data-role-band="lead"]{border-left-color:#f0883e}
|
||||||
|
.role-pill{display:inline-block;font-size:9px;padding:1px 7px;border-radius:3px;background:#0d1117;color:#8b949e;margin-right:6px;font-weight:600;letter-spacing:0.4px;text-transform:uppercase;border-left:2px solid #30363d;vertical-align:1px}
|
||||||
|
.role-pill[data-rb="warehouse"]{border-left-color:#58a6ff;color:#79c0ff}
|
||||||
|
.role-pill[data-rb="production"]{border-left-color:#d29922;color:#e3b341}
|
||||||
|
.role-pill[data-rb="trades"]{border-left-color:#bc8cff;color:#d2a8ff}
|
||||||
|
.role-pill[data-rb="driver"]{border-left-color:#3fb950;color:#56d364}
|
||||||
|
.role-pill[data-rb="lead"]{border-left-color:#f0883e;color:#ffa657}
|
||||||
.worker .info{flex:1;min-width:0}
|
.worker .info{flex:1;min-width:0}
|
||||||
.worker .nm{color:#e6edf3;font-weight:500}
|
.worker .nm{color:#e6edf3;font-weight:500}
|
||||||
.worker .why{color:#545d68;font-size:11px;margin-top:1px}
|
.worker .why{color:#545d68;font-size:11px;margin-top:1px}
|
||||||
@ -95,6 +109,7 @@ details .body{padding-top:10px;font-size:12px;color:#8b949e}
|
|||||||
<nav>
|
<nav>
|
||||||
<a href=".">Dashboard</a>
|
<a href=".">Dashboard</a>
|
||||||
<a href="console" class="active">Walkthrough</a>
|
<a href="console" class="active">Walkthrough</a>
|
||||||
|
<a href="profiler">Profiler</a>
|
||||||
<a href="proof">Architecture</a>
|
<a href="proof">Architecture</a>
|
||||||
<a href="spec">Spec</a>
|
<a href="spec">Spec</a>
|
||||||
<a href="onboard">Onboard</a>
|
<a href="onboard">Onboard</a>
|
||||||
@ -147,11 +162,40 @@ details .body{padding-top:10px;font-size:12px;color:#8b949e}
|
|||||||
|
|
||||||
<div class="chapter">
|
<div class="chapter">
|
||||||
<div class="num">Chapter 6</div>
|
<div class="num">Chapter 6</div>
|
||||||
<h2>Try it yourself</h2>
|
<h2>Three coordinators, three views of the same corpus</h2>
|
||||||
<div class="lede">Type any staffing question. The system picks the right search path (smart-parse, semantic discovery, analytics), shows what it understood, and returns ranked results with memory signal.</div>
|
<div class="lede">Maria runs Chicago, Devon runs Indianapolis, Aisha runs Milwaukee. Same database, same playbooks — but the search results, the recurring-skill patterns, and the playbook context all reshape to whoever is acting. This is the per-staffer hot-swap index: the relevance gradient is unique to each person, and gets sharper the more they use it.</div>
|
||||||
|
<div id="ch6-staffers"><div class="loading">Loading staffer roster…</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chapter">
|
||||||
|
<div class="num">Chapter 7</div>
|
||||||
|
<h2>The hidden signal — public issuers in your contractor graph</h2>
|
||||||
|
<div class="lede">Every contractor in this corpus is also a forward indicator on the public equities they touch. Permit filings precede construction starts by ~45 days, staffing windows by ~30, revenue recognition by months. The associated-ticker network surfaces this signal <em>before</em> any 10-Q. Below: the top issuers attributable to the contractor activity in this view, with live prices.</div>
|
||||||
|
<div id="ch7-signal"><div class="loading">Computing the Building Activity Index…</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chapter">
|
||||||
|
<div class="num">Chapter 8</div>
|
||||||
|
<h2>When something breaks — triage in one shot</h2>
|
||||||
|
<div class="lede">A coordinator gets a text: "Marcus running late." Watch what the system does in 250 milliseconds: pulls Marcus's record, scores his attendance pattern, finds five same-role same-geo backfills sorted by responsiveness, and pre-writes the SMS to send to the client. This is the moment the AI becomes worth its weight.</div>
|
||||||
|
<div id="ch8-triage"><div class="loading">Running the triage scenario…</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chapter">
|
||||||
|
<div class="num">Chapter 9</div>
|
||||||
|
<h2>Try it yourself — every input below hits a different route</h2>
|
||||||
|
<div class="lede">Type any staffing question. The router picks the right path: smart-parse (zip code, headcount, role, state), semantic discovery, name lookup, late-worker triage, "what came in last night" temporal queries. Whatever you type, the system tells you what it understood and how it routed.</div>
|
||||||
<div class="try-box">
|
<div class="try-box">
|
||||||
<input type="text" id="try-q" placeholder="e.g. reliable forklift operators in Chicago with OSHA certs" onkeydown="if(event.key==='Enter')runTry()">
|
<input type="text" id="try-q" placeholder="e.g. 8 production workers near 60607 by next Friday" onkeydown="if(event.key==='Enter')runTry()">
|
||||||
<button id="try-btn" onclick="runTry()">Ask</button>
|
<button id="try-btn" onclick="runTry()">Ask</button>
|
||||||
|
<div style="margin-top:10px;font-size:11px;color:#545d68;line-height:1.7">
|
||||||
|
Try one of these to see different routes fire:<br>
|
||||||
|
<a href="#" onclick="document.getElementById('try-q').value='8 production workers near 60607';runTry();return false">8 production workers near 60607</a> ·
|
||||||
|
<a href="#" onclick="document.getElementById('try-q').value='Marcus running late site 4422';runTry();return false">Marcus running late site 4422</a> ·
|
||||||
|
<a href="#" onclick="document.getElementById('try-q').value='Marcus';runTry();return false">Marcus</a> ·
|
||||||
|
<a href="#" onclick="document.getElementById('try-q').value='what came in last night';runTry();return false">what came in last night</a> ·
|
||||||
|
<a href="#" onclick="document.getElementById('try-q').value='reliable forklift operators with OSHA certs';runTry();return false">reliable forklift operators with OSHA certs</a>
|
||||||
|
</div>
|
||||||
<div id="try-out" style="margin-top:16px"></div>
|
<div id="try-out" style="margin-top:16px"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -167,6 +211,114 @@ var A=location.origin+P;
|
|||||||
// DOM helpers — all dynamic content goes through these. No innerHTML
|
// DOM helpers — all dynamic content goes through these. No innerHTML
|
||||||
// anywhere in the script; every API-derived string passes through
|
// anywhere in the script; every API-derived string passes through
|
||||||
// textContent so no injection path regardless of upstream data.
|
// textContent so no injection path regardless of upstream data.
|
||||||
|
// Role classification — mirrors search.html, no emojis. Maps role
|
||||||
|
// strings to a band+label used by the worker-card border + role pill.
|
||||||
|
var ROLE_BANDS = [
|
||||||
|
{ match: /forklift|warehouse|associate|material\s*handler|loader|loading|packag|shipping|logistics|inventory|sanitation|janit/i, band: 'warehouse', label: 'Warehouse' },
|
||||||
|
{ match: /production|assembl/i, band: 'production', label: 'Production' },
|
||||||
|
{ match: /welder|weld|electric|maint(enance)?\s*tech|cnc|machine\s*op|hvac|plumb|carpenter|mason/i, band: 'trades', label: 'Skilled Trade' },
|
||||||
|
{ match: /driver|truck|haul|cdl/i, band: 'driver', label: 'Driver' },
|
||||||
|
{ match: /line\s*lead|supervisor|foreman|coordinator/i, band: 'lead', label: 'Lead' },
|
||||||
|
{ match: /quality/i, band: 'production', label: 'Quality' },
|
||||||
|
];
|
||||||
|
function roleBand(role){
|
||||||
|
if(!role) return { band: 'warehouse', label: '' };
|
||||||
|
for (var i = 0; i < ROLE_BANDS.length; i++) {
|
||||||
|
if (ROLE_BANDS[i].match.test(role)) return ROLE_BANDS[i];
|
||||||
|
}
|
||||||
|
return { band: 'warehouse', label: role.split(' ')[0].toUpperCase().slice(0, 12) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a sober worker card: monogram avatar + colored role band on
|
||||||
|
// the left edge + uppercase role pill in the detail line. Used by
|
||||||
|
// every chapter that renders worker rows. `name` and `role` drive the
|
||||||
|
// classification; `detail` is the full text after the pill.
|
||||||
|
// Quick first-name → gender hint for face-pool selection. Same lookup
|
||||||
|
// idea as the dashboard; if the name is unknown, the server falls back
|
||||||
|
// to the full pool. Trimmed table — covers the most common names that
|
||||||
|
// appear in the synthetic worker data.
|
||||||
|
var FEMALE_NAMES = new Set(['Mary','Patricia','Jennifer','Linda','Elizabeth','Barbara','Susan','Jessica','Sarah','Karen','Lisa','Nancy','Betty','Sandra','Margaret','Ashley','Kimberly','Emily','Donna','Michelle','Carol','Amanda','Melissa','Deborah','Stephanie','Dorothy','Rebecca','Sharon','Laura','Cynthia','Amy','Kathleen','Angela','Shirley','Brenda','Emma','Anna','Pamela','Nicole','Samantha','Katherine','Christine','Helen','Debra','Rachel','Carolyn','Janet','Maria','Catherine','Heather','Diane','Olivia','Julie','Joyce','Victoria','Ruth','Virginia','Lauren','Kelly','Christina','Joan','Evelyn','Judith','Andrea','Hannah','Megan','Cheryl','Jacqueline','Martha','Madison','Teresa','Gloria','Sara','Janice','Ann','Kathryn','Abigail','Sophia','Frances','Jean','Alice','Judy','Isabella','Julia','Grace','Amber','Denise','Danielle','Marilyn','Beverly','Charlotte','Natalie','Theresa','Diana','Brittany','Kayla','Alexis','Lori','Marie','Carmen','Aisha','Rosa','Mia','Audrey','Erin','Tina','Vanessa','Tara','Wendy','Tanya','Maya','Crystal','Yvonne','Kara','Shannon','Brianna','Faith','Caroline','Carla','Tracey','Tracy','Rita','Dawn','Tiffany','Stacy','Stacey','Gina','Bonnie','Tammy','Joanne','Jamie','Tonya','Alyssa','Ariana','Elena','Ellie','Erica','Erika','Felicia','Holly','Jenna','Jenny','Krista','Kristen','Kristin','Krystal','Lana','Leah','Lucy','Mallory','Melinda','Meredith','Misty','Monica','Naomi','Paige','Paula','Renee','Rhonda','Robin','Roxanne','Selena','Sierra','Skylar','Sonia','Stella','Tamara','Veronica','Vivian','Whitney','Yolanda','Zoe']);
|
||||||
|
var MALE_NAMES = new Set(['James','Robert','John','Michael','David','William','Richard','Joseph','Thomas','Charles','Christopher','Daniel','Matthew','Anthony','Mark','Donald','Steven','Paul','Andrew','Joshua','Kenneth','Kevin','Brian','George','Edward','Ronald','Timothy','Jason','Jeffrey','Ryan','Jacob','Gary','Nicholas','Eric','Jonathan','Stephen','Larry','Justin','Scott','Brandon','Benjamin','Samuel','Gregory','Frank','Alexander','Raymond','Patrick','Jack','Dennis','Jerry','Tyler','Aaron','Jose','Adam','Henry','Nathan','Douglas','Zachary','Peter','Kyle','Walter','Ethan','Jeremy','Harold','Keith','Christian','Roger','Noah','Gerald','Carl','Terry','Sean','Austin','Arthur','Lawrence','Jesse','Dylan','Bryan','Joe','Jordan','Billy','Bruce','Albert','Willie','Gabriel','Logan','Alan','Juan','Wayne','Roy','Ralph','Randy','Eugene','Vincent','Russell','Elijah','Louis','Bobby','Philip','Johnny','Marcus','Antonio','Carlos','Diego','Hector','Jorge','Julio','Manuel','Miguel','Pedro','Raul','Ricardo','Roberto','Sergio','Victor','Jamal','Xavier','DeShawn','Dwayne','Jermaine','Malik','Tyrone','Devon','Andre','Brent','Calvin','Casey','Cody','Cole','Cory','Dale','Damon','Darius','Darrell','Dean','Derek','Drew','Earl','Eddie','Floyd','Glenn','Greg','Howard','Ivan','Jared','Jay','Jeff','Joel','Lance','Lee','Leonard','Lloyd','Mario','Martin','Mason','Maurice','Max','Mitchell','Morgan','Nick','Norman','Oliver','Owen','Pete','Quincy','Rafael','Reggie','Rex','Ricky','Russ','Shane','Shaun','Stanley','Steve','Theodore','Todd','Travis','Trevor','Troy','Wade','Warren','Wesley']);
|
||||||
|
function guessGenderFromFirstName(n){
|
||||||
|
if(!n) return null;
|
||||||
|
var clean=n.replace(/[^A-Za-z]/g,'');
|
||||||
|
if(!clean) return null;
|
||||||
|
var c=clean[0].toUpperCase()+clean.slice(1).toLowerCase();
|
||||||
|
if(FEMALE_NAMES.has(c)) return 'woman';
|
||||||
|
if(MALE_NAMES.has(c)) return 'man';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function genderFor(name){
|
||||||
|
var g = guessGenderFromFirstName(name);
|
||||||
|
if(g) return g;
|
||||||
|
if(!name) return 'man';
|
||||||
|
var s=String(name); var h=0;
|
||||||
|
for(var i=0;i<s.length;i++) h=(h*31+s.charCodeAt(i))|0;
|
||||||
|
return (Math.abs(h)&1)?'man':'woman';
|
||||||
|
}
|
||||||
|
// Confident first-name → ethnicity. Synthetic data — we own the call.
|
||||||
|
var NAMES_SOUTH_ASIAN_C=new Set(['Raj','Anil','Rohan','Vikram','Arjun','Sanjay','Ravi','Krishna','Pradeep','Sunil','Amit','Deepak','Ashok','Manoj','Rahul','Vijay','Suresh','Naveen','Anand','Nikhil','Aditya','Karan','Rajesh','Priya','Anjali','Neha','Kavya','Pooja','Divya','Meera','Lakshmi','Rani','Asha','Saanvi','Aanya','Aaradhya','Shreya','Riya','Tanvi','Ishita','Aarav','Ishaan','Shivani']);
|
||||||
|
var NAMES_EAST_ASIAN_C=new Set(['Wei','Mei','Yi','Jin','Chen','Lin','Liu','Wang','Zhang','Yang','Wu','Zhao','Sun','Hiroshi','Yuki','Akira','Kenji','Sakura','Aiko','Haruto','Sora','Hyun','Eun','Yoon','Kai','Long','Hong','Xiu','Lan','Hua','Hao','Tao','Bao','Cheng','Feng','Jian','Dong','Bin','Min','Lei','Hui','Yu','Xin','Ying','Zhen','Yuan','Yan']);
|
||||||
|
var NAMES_HISPANIC_C=new Set(['Carmen','Carlos','Maria','Diego','Hector','Jorge','Julio','Manuel','Miguel','Pedro','Raul','Ricardo','Roberto','Sergio','Antonio','Esperanza','Luz','Sofia','Lucia','Isabella','Camila','Valentina','Mariana','Elena','Rosa','Catalina','Esteban','Fernando','Eduardo','Javier','Alejandro','Andres','Mateo','Santiago','Sebastian','Emilio','Tomas','Cristina','Daniela','Gabriela','Ximena','Adriana','Beatriz','Pilar','Mercedes','Xavier','Marisol','Guadalupe','Lupita','Inez','Itzel','Yesenia','Joaquin','Ignacio','Rafael','Salvador','Cesar','Arturo','Armando','Hugo','Marco','Alejandra','Felipe','Gerardo','Jaime','Leonardo','Luis','Pablo','Ramon']);
|
||||||
|
var NAMES_BLACK_C=new Set(['DeShawn','Jamal','Aisha','Latoya','Tyrone','Malik','Imani','Keisha','Tariq','Lakisha','Kenya','Tamika','Andre','Marcus','Demetrius','Jermaine','Reggie','Tyrese','Darius','Trevon','Kareem','Damon','Jalen','Jaylen','Dwayne','DaQuan','Aaliyah','Kiara','Janelle','Jasmine','Tanisha','Maurice','Tyrell','Kwame','Khalil','Terrell','Cedric','Nia','Zuri','Jada','Ebony','Dominique']);
|
||||||
|
var NAMES_MIDDLE_EASTERN_C=new Set(['Layla','Omar','Khalid','Fatima','Yasmin','Hassan','Hussein','Ahmed','Mohamed','Mohammed','Ali','Karim','Yusuf','Yara','Nadia','Zainab','Rania','Samira','Mariam','Salma','Ibrahim','Mahmoud','Saif','Anwar','Bilal','Faisal','Hamza','Imran','Sami','Wael','Zaid','Amira','Iman','Lina','Mona','Noor','Rana','Soha','Zara']);
|
||||||
|
function guessEthnicityFromFirstName(n){
|
||||||
|
if(!n) return 'caucasian';
|
||||||
|
var clean=n.replace(/[^A-Za-z]/g,''); if(!clean) return 'caucasian';
|
||||||
|
var c=clean[0].toUpperCase()+clean.slice(1).toLowerCase();
|
||||||
|
if(NAMES_MIDDLE_EASTERN_C.has(c)) return 'middle_eastern';
|
||||||
|
if(NAMES_BLACK_C.has(c)) return 'black';
|
||||||
|
if(NAMES_HISPANIC_C.has(c)) return 'hispanic';
|
||||||
|
if(NAMES_SOUTH_ASIAN_C.has(c)) return 'south_asian';
|
||||||
|
if(NAMES_EAST_ASIAN_C.has(c)) return 'east_asian';
|
||||||
|
return 'caucasian';
|
||||||
|
}
|
||||||
|
|
||||||
|
function workerRow(name, role, detail, opts){
|
||||||
|
opts = opts || {};
|
||||||
|
var band = roleBand(role||'');
|
||||||
|
var w = el('div','worker');
|
||||||
|
if(band.band) w.dataset.roleBand = band.band;
|
||||||
|
var initials = (name||'?').split(' ').map(function(s){return (s[0]||'').toUpperCase()}).join('').substring(0,2);
|
||||||
|
var av = el('div','av',initials);
|
||||||
|
// Real synthetic headshot via /headshots/<key>; deterministic so
|
||||||
|
// same worker always gets the same face. Falls back to monogram if
|
||||||
|
// pool isn't fetched yet.
|
||||||
|
var faceKey = (opts.face_key) || name || '';
|
||||||
|
var firstName = (name||'').split(/\s+/)[0]||'';
|
||||||
|
var gHint = genderFor(firstName);
|
||||||
|
var eHint = guessEthnicityFromFirstName(firstName);
|
||||||
|
if(faceKey){
|
||||||
|
var img=document.createElement('img');
|
||||||
|
img.alt='';
|
||||||
|
img.loading='lazy';
|
||||||
|
img.src = P + '/headshots/' + encodeURIComponent(faceKey) + '?g='+gHint+'&e='+eHint;
|
||||||
|
img.onerror=function(){ this.remove(); };
|
||||||
|
av.appendChild(img);
|
||||||
|
}
|
||||||
|
w.appendChild(av);
|
||||||
|
var info = el('div','info');
|
||||||
|
var nm = el('div','nm', name||'?');
|
||||||
|
if(opts.endorsed){
|
||||||
|
nm.appendChild(el('span','boost-chip',opts.endorsed));
|
||||||
|
}
|
||||||
|
info.appendChild(nm);
|
||||||
|
var why = el('div','why');
|
||||||
|
if(band.label){
|
||||||
|
var pill = document.createElement('span'); pill.className='role-pill';
|
||||||
|
pill.dataset.rb = band.band;
|
||||||
|
pill.textContent = band.label;
|
||||||
|
why.appendChild(pill);
|
||||||
|
}
|
||||||
|
why.appendChild(document.createTextNode(detail||''));
|
||||||
|
info.appendChild(why);
|
||||||
|
w.appendChild(info);
|
||||||
|
if(opts.score){
|
||||||
|
w.appendChild(el('div','score', opts.score));
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
function el(tag, cls, text){
|
function el(tag, cls, text){
|
||||||
var e=document.createElement(tag);
|
var e=document.createElement(tag);
|
||||||
if(cls) e.className=cls;
|
if(cls) e.className=cls;
|
||||||
@ -191,6 +343,9 @@ window.addEventListener('load',function(){
|
|||||||
loadChapter3();
|
loadChapter3();
|
||||||
loadChapter4();
|
loadChapter4();
|
||||||
loadChapter5();
|
loadChapter5();
|
||||||
|
loadChapter6();
|
||||||
|
loadChapter7();
|
||||||
|
loadChapter8();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Chapter 1 ────────────────────────────────────────────
|
// ─── Chapter 1 ────────────────────────────────────────────
|
||||||
@ -306,6 +461,30 @@ function loadChapter4(){
|
|||||||
addr.style.cssText='color:#8b949e;font-size:12px;margin-top:2px';
|
addr.style.cssText='color:#8b949e;font-size:12px;margin-top:2px';
|
||||||
card.appendChild(addr);
|
card.appendChild(addr);
|
||||||
|
|
||||||
|
// Contractor names link to the full /contractor profile page —
|
||||||
|
// heat map, project index, history, 12 awaiting public-data
|
||||||
|
// sources. The staffer click-through J asked for.
|
||||||
|
if(p.contact_1_name || p.contact_2_name){
|
||||||
|
var contractors=document.createElement('div');
|
||||||
|
contractors.style.cssText='color:#8b949e;font-size:12px;margin-top:4px';
|
||||||
|
contractors.appendChild(document.createTextNode('Contractors: '));
|
||||||
|
var seen=[];
|
||||||
|
[p.contact_1_name, p.contact_2_name].forEach(function(n,i){
|
||||||
|
if(!n || seen.indexOf(n)>=0) return;
|
||||||
|
seen.push(n);
|
||||||
|
if(seen.length>1) contractors.appendChild(document.createTextNode(' · '));
|
||||||
|
var a=document.createElement('a');
|
||||||
|
a.href=P+'/contractor?name='+encodeURIComponent(n);
|
||||||
|
a.target='_blank';
|
||||||
|
a.rel='noopener';
|
||||||
|
a.style.cssText='color:#58a6ff;text-decoration:none;border-bottom:1px dotted #58a6ff44';
|
||||||
|
a.title='Open full contractor profile';
|
||||||
|
a.textContent=n;
|
||||||
|
contractors.appendChild(a);
|
||||||
|
});
|
||||||
|
card.appendChild(contractors);
|
||||||
|
}
|
||||||
|
|
||||||
card.appendChild(el('div','step-label','STEP 1 · Derive staffing need'));
|
card.appendChild(el('div','step-label','STEP 1 · Derive staffing need'));
|
||||||
var s1=el('div','step-body');
|
var s1=el('div','step-body');
|
||||||
s1.appendChild(document.createTextNode('Industry heuristic: ~1 worker per $150K of permit cost, capped 2-8. Resulting contract: '));
|
s1.appendChild(document.createTextNode('Industry heuristic: ~1 worker per $150K of permit cost, capped 2-8. Resulting contract: '));
|
||||||
@ -321,21 +500,13 @@ function loadChapter4(){
|
|||||||
|
|
||||||
var list=document.createElement('div');list.style.marginTop='6px';
|
var list=document.createElement('div');list.style.marginTop='6px';
|
||||||
(prop.candidates||[]).slice(0,5).forEach(function(cand,i){
|
(prop.candidates||[]).slice(0,5).forEach(function(cand,i){
|
||||||
var w=el('div','worker');
|
var detail = cand.doc_id+' · '+(cand.playbook_boost>0?'boosted +'+cand.playbook_boost.toFixed(3)+' by memory · ':'')+'semantic score '+(cand.score||0).toFixed(3);
|
||||||
var initials=(cand.name||'?').split(' ').map(function(s){return (s[0]||'').toUpperCase()}).join('').substring(0,2);
|
var endorsed = (cand.playbook_boost||0) > 0
|
||||||
w.appendChild(el('div','av',initials));
|
? 'Endorsed · '+((cand.playbook_citations||[]).length)+' past fill'+((cand.playbook_citations||[]).length!==1?'s':'')
|
||||||
var info=el('div','info');
|
: null;
|
||||||
var nm=el('div','nm',cand.name||cand.doc_id||'?');
|
list.appendChild(workerRow(cand.name||cand.doc_id||'?', prop.role||'', detail, {
|
||||||
if((cand.playbook_boost||0)>0){
|
endorsed: endorsed, score: '#'+(i+1)
|
||||||
var ncit=(cand.playbook_citations||[]).length;
|
}));
|
||||||
nm.appendChild(el('span','boost-chip','Endorsed · '+ncit+' past fill'+(ncit!==1?'s':'')));
|
|
||||||
}
|
|
||||||
info.appendChild(nm);
|
|
||||||
var why=cand.doc_id+' · '+(cand.playbook_boost>0?'boosted +'+cand.playbook_boost.toFixed(3)+' by memory · ':'')+'semantic score '+(cand.score||0).toFixed(3);
|
|
||||||
info.appendChild(el('div','why',why));
|
|
||||||
w.appendChild(info);
|
|
||||||
w.appendChild(el('div','score','#'+(i+1)));
|
|
||||||
list.appendChild(w);
|
|
||||||
});
|
});
|
||||||
card.appendChild(list);
|
card.appendChild(list);
|
||||||
|
|
||||||
@ -407,7 +578,182 @@ function loadChapter5(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Chapter 6 ────────────────────────────────────────────
|
// ─── Chapter 6 — per-staffer hot-swap ─────────────────────
|
||||||
|
function loadChapter6(){
|
||||||
|
apiGet('/staffers').then(function(r){
|
||||||
|
var host=document.getElementById('ch6-staffers');host.textContent='';
|
||||||
|
var staffers=(r&&r.staffers)||[];
|
||||||
|
if(!staffers.length){
|
||||||
|
host.appendChild(el('div','err','No staffer roster — /staffers returned empty.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var grid=document.createElement('div'); grid.className='grid'; grid.style.gridTemplateColumns='repeat(auto-fit,minmax(280px,1fr))';
|
||||||
|
staffers.forEach(function(s){
|
||||||
|
var card=el('div','card accent-b');
|
||||||
|
var name=el('div',null,s.name);
|
||||||
|
name.style.cssText='font-size:18px;font-weight:700;color:#e6edf3;letter-spacing:-0.3px';
|
||||||
|
card.appendChild(name);
|
||||||
|
var role=el('div',null,s.display||'');
|
||||||
|
role.style.cssText='font-size:11px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;margin-top:2px';
|
||||||
|
card.appendChild(role);
|
||||||
|
var ter=el('div',null,'Territory: '+s.territory.state+' · '+s.territory.cities.slice(0,3).join(', ')+'…');
|
||||||
|
ter.style.cssText='color:#8b949e;font-size:12px;margin-top:8px';
|
||||||
|
card.appendChild(ter);
|
||||||
|
var greet=el('div',null,s.greeting||'');
|
||||||
|
greet.style.cssText='color:#c9d1d9;font-size:11px;margin-top:6px;line-height:1.5;border-top:1px dashed #1f2631;padding-top:6px';
|
||||||
|
card.appendChild(greet);
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
host.appendChild(grid);
|
||||||
|
var narr=el('div','narr');
|
||||||
|
narr.appendChild(el('strong',null,'What this means for a staffer. '));
|
||||||
|
narr.appendChild(document.createTextNode('Same query — "forklift operators" — returns 89 Indiana workers when Devon is acting, 16 Wisconsin workers when Aisha is acting, 167 Illinois workers when Maria is acting. The MEMORY panel relabels itself with whoever\'s viewing. The corpus stays intact; the relevance gradient is per coordinator. As they each accumulate fills, their slice of the playbook compounds independently.'));
|
||||||
|
host.appendChild(narr);
|
||||||
|
}).catch(function(e){
|
||||||
|
var h=document.getElementById('ch6-staffers');h.textContent='';h.appendChild(el('div','err','Staffer roster unavailable: '+(e.message||e)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chapter 7 — Construction Activity Signal Engine ──────
|
||||||
|
function loadChapter7(){
|
||||||
|
Promise.all([
|
||||||
|
api('/intelligence/profiler_index',{limit:200}),
|
||||||
|
]).then(function(rs){
|
||||||
|
var prof=rs[0]||{};
|
||||||
|
var rows=prof.contractors||[];
|
||||||
|
var host=document.getElementById('ch7-signal');host.textContent='';
|
||||||
|
// Aggregate basket
|
||||||
|
var byTicker={};
|
||||||
|
rows.forEach(function(r){
|
||||||
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||||||
|
ts.forEach(function(t){
|
||||||
|
if(!t||!t.ticker) return;
|
||||||
|
if(!byTicker[t.ticker]) byTicker[t.ticker]={ticker:t.ticker,count:0,kinds:new Set()};
|
||||||
|
byTicker[t.ticker].count++;
|
||||||
|
byTicker[t.ticker].kinds.add(t.via);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var basket=Object.values(byTicker).sort(function(a,b){return b.count-a.count});
|
||||||
|
var attribCost=0;
|
||||||
|
rows.forEach(function(r){
|
||||||
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||||||
|
if(ts.length>0) attribCost += (r.total_cost||0);
|
||||||
|
});
|
||||||
|
var totalAttrib = basket.reduce(function(s,b){return s+b.count},0);
|
||||||
|
if(!basket.length){
|
||||||
|
host.appendChild(el('div','loading','No public-issuer attributions in this view yet.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Top-line metric strip
|
||||||
|
var grid=document.createElement('div');grid.className='grid';
|
||||||
|
var c1=el('div','card accent-g');
|
||||||
|
var b1=el('div',null,basket.length); b1.style.cssText='font-size:30px;font-weight:800;color:#3fb950;line-height:1';
|
||||||
|
c1.appendChild(b1);
|
||||||
|
var l1=el('div',null,'Public issuers in scope'); l1.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;margin-top:8px;font-weight:600';
|
||||||
|
c1.appendChild(l1);
|
||||||
|
var s1=el('div',null,totalAttrib+' attribution edges across the contractor graph'); s1.style.cssText='font-size:12px;color:#8b949e;margin-top:4px';
|
||||||
|
c1.appendChild(s1);
|
||||||
|
grid.appendChild(c1);
|
||||||
|
var c2=el('div','card accent-b');
|
||||||
|
var bav = attribCost>=1e9?'$'+(attribCost/1e9).toFixed(2)+'B':attribCost>=1e6?'$'+(attribCost/1e6).toFixed(0)+'M':'$'+Math.round(attribCost/1e3)+'K';
|
||||||
|
var b2=el('div',null,bav); b2.style.cssText='font-size:30px;font-weight:800;color:#58a6ff;line-height:1';
|
||||||
|
c2.appendChild(b2);
|
||||||
|
var l2=el('div',null,'Attributed build value'); l2.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;margin-top:8px;font-weight:600';
|
||||||
|
c2.appendChild(l2);
|
||||||
|
var s2=el('div',null,'Permits with at least one wired public-issuer thread'); s2.style.cssText='font-size:12px;color:#8b949e;margin-top:4px';
|
||||||
|
c2.appendChild(s2);
|
||||||
|
grid.appendChild(c2);
|
||||||
|
var c3=el('div','card accent-l');
|
||||||
|
var b3=el('div',null,rows.length); b3.style.cssText='font-size:30px;font-weight:800;color:#bc8cff;line-height:1';
|
||||||
|
c3.appendChild(b3);
|
||||||
|
var l3=el('div',null,'Contractors indexed'); l3.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;margin-top:8px;font-weight:600';
|
||||||
|
c3.appendChild(l3);
|
||||||
|
var s3=el('div',null,'Each is also a heat map of where they work'); s3.style.cssText='font-size:12px;color:#8b949e;margin-top:4px';
|
||||||
|
c3.appendChild(s3);
|
||||||
|
grid.appendChild(c3);
|
||||||
|
host.appendChild(grid);
|
||||||
|
// Top issuer table
|
||||||
|
var tHdr=document.createElement('div');tHdr.style.cssText='color:#545d68;font-size:11px;text-transform:uppercase;letter-spacing:1.4px;font-weight:600;margin:14px 0 8px';
|
||||||
|
tHdr.textContent='Top public issuers attributable in this view';
|
||||||
|
host.appendChild(tHdr);
|
||||||
|
basket.slice(0,8).forEach(function(b){
|
||||||
|
var row=el('div','row');
|
||||||
|
var left=document.createElement('div');left.style.flex='1';left.style.minWidth='0';
|
||||||
|
var tk=el('div','title',b.ticker);
|
||||||
|
tk.style.cssText+='font-family:ui-monospace,monospace;color:#3fb950';
|
||||||
|
left.appendChild(tk);
|
||||||
|
var kinds=Array.from(b.kinds);
|
||||||
|
var meta=el('div','meta',b.count+' attribution'+(b.count===1?'':'s')+' · '+kinds.join('+'));
|
||||||
|
left.appendChild(meta);
|
||||||
|
row.appendChild(left);
|
||||||
|
var right=document.createElement('div');right.style.cssText='font-size:11px;color:#58a6ff';
|
||||||
|
var a=document.createElement('a');a.href=P+'/profiler';a.target='_blank';a.style.color='#58a6ff';a.style.textDecoration='none';
|
||||||
|
a.textContent='see in profiler →';
|
||||||
|
right.appendChild(a);
|
||||||
|
row.appendChild(right);
|
||||||
|
host.appendChild(row);
|
||||||
|
});
|
||||||
|
var narr=el('div','narr');
|
||||||
|
narr.appendChild(el('strong',null,'What this means for the business. '));
|
||||||
|
narr.appendChild(document.createTextNode('The data corpus is also a market-signal engine. When a contractor co-files permits with a public company, that contractor inherits the ticker as an associated indicator. Permit volume changes precede earnings calls by months. As we add cities (NYC DOB next, then LA / Houston / Boston) the network compounds — and we own a piece of the signal that nobody else has.'));
|
||||||
|
host.appendChild(narr);
|
||||||
|
}).catch(function(e){
|
||||||
|
var h=document.getElementById('ch7-signal');h.textContent='';h.appendChild(el('div','err','Signal engine unavailable: '+(e.message||e)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chapter 8 — Triage in one shot ───────────────────────
|
||||||
|
function loadChapter8(){
|
||||||
|
api('/intelligence/chat',{message:'Marcus running late site 4422'}).then(function(d){
|
||||||
|
var host=document.getElementById('ch8-triage');host.textContent='';
|
||||||
|
if(d.type!=='triage'){
|
||||||
|
host.appendChild(el('div','err','Triage route did not fire. Got type=' + (d.type||'?')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Worker card
|
||||||
|
var wc=el('div','card accent-r');
|
||||||
|
var lbl=el('div',null,'⚠ TRIAGE EVENT'); lbl.style.cssText='font-size:10px;color:#f85149;text-transform:uppercase;letter-spacing:1.2px;font-weight:700;margin-bottom:8px';
|
||||||
|
wc.appendChild(lbl);
|
||||||
|
var nm=el('div',null,d.worker.name); nm.style.cssText='font-size:18px;font-weight:700;color:#e6edf3';
|
||||||
|
wc.appendChild(nm);
|
||||||
|
var loc=el('div',null,(d.worker.role||'?')+' · '+(d.worker.city||'')+', '+(d.worker.state||''));
|
||||||
|
loc.style.cssText='font-size:12px;color:#8b949e;margin-top:2px';
|
||||||
|
wc.appendChild(loc);
|
||||||
|
var stats=document.createElement('div');stats.style.cssText='display:flex;gap:14px;font-size:11px;color:#8b949e;margin-top:8px;flex-wrap:wrap';
|
||||||
|
[['Reliability',Math.round((d.worker.rel||0)*100)+'%'],['Responsiveness',Math.round((d.worker.resp||0)*100)+'%'],['Availability',Math.round((d.worker.avail||0)*100)+'%']].forEach(function(p){
|
||||||
|
var s=document.createElement('span');
|
||||||
|
var l=document.createElement('span');l.textContent=p[0]+': ';
|
||||||
|
var b=document.createElement('b');b.style.color='#e6edf3';b.textContent=p[1];
|
||||||
|
s.appendChild(l);s.appendChild(b);stats.appendChild(s);
|
||||||
|
});
|
||||||
|
wc.appendChild(stats);
|
||||||
|
host.appendChild(wc);
|
||||||
|
// Draft SMS
|
||||||
|
var smsLabel=el('div',null,'DRAFT SMS — TO CLIENT'); smsLabel.style.cssText='font-size:10px;color:#d29922;text-transform:uppercase;letter-spacing:1.2px;font-weight:700;margin:14px 0 4px';
|
||||||
|
host.appendChild(smsLabel);
|
||||||
|
var smsBox=el('div',null,d.draft_sms||'');
|
||||||
|
smsBox.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:10px 12px;font-family:ui-monospace,monospace;font-size:12px;color:#e6edf3;line-height:1.5;white-space:pre-wrap';
|
||||||
|
host.appendChild(smsBox);
|
||||||
|
// Backfills
|
||||||
|
if((d.backfills||[]).length){
|
||||||
|
var bfHdr=document.createElement('div');bfHdr.style.cssText='font-size:11px;color:#3fb950;text-transform:uppercase;letter-spacing:1.2px;font-weight:600;margin:14px 0 8px';
|
||||||
|
bfHdr.textContent='✓ '+d.backfills.length+' local '+(d.worker.role||'workers')+' available — sorted by responsiveness';
|
||||||
|
host.appendChild(bfHdr);
|
||||||
|
d.backfills.slice(0,5).forEach(function(c){
|
||||||
|
var detail=(c.role||'?')+' · '+(c.city||'')+', '+(c.state||'')+' · rel '+Math.round((c.rel||0)*100)+'% · resp '+Math.round((c.resp||0)*100)+'%';
|
||||||
|
host.appendChild(workerRow(c.name||'?', c.role||'', detail));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var narr=el('div','narr');
|
||||||
|
narr.appendChild(el('strong',null,'What this means for a coordinator. '));
|
||||||
|
narr.appendChild(document.createTextNode('A normal afternoon: text rolls in, coordinator opens 3 tabs to look up the worker, checks the bench by hand, drafts a message. 20 minutes. Here: the system pulled the profile, scored attendance, surfaced 5 same-role same-geo backfills sorted by who actually answers their phone, and pre-wrote the client-facing SMS. The coordinator clicks send. ' + d.duration_ms + 'ms.'));
|
||||||
|
host.appendChild(narr);
|
||||||
|
}).catch(function(e){
|
||||||
|
var h=document.getElementById('ch8-triage');h.textContent='';h.appendChild(el('div','err','Triage demo unavailable: '+(e.message||e)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chapter 9 (was 6) — Try it yourself ──────────────────
|
||||||
function runTry(){
|
function runTry(){
|
||||||
var q=document.getElementById('try-q').value.trim();if(!q)return;
|
var q=document.getElementById('try-q').value.trim();if(!q)return;
|
||||||
var btn=document.getElementById('try-btn'),out=document.getElementById('try-out');
|
var btn=document.getElementById('try-btn'),out=document.getElementById('try-out');
|
||||||
@ -437,23 +783,16 @@ function runTry(){
|
|||||||
|
|
||||||
var workers=d.sql_results||d.vector_results||d.results||[];
|
var workers=d.sql_results||d.vector_results||d.results||[];
|
||||||
workers.slice(0,5).forEach(function(w,i){
|
workers.slice(0,5).forEach(function(w,i){
|
||||||
var row=el('div','worker');
|
|
||||||
var nm=w.name||(w.text||'').split('—')[0].trim()||w.doc_id||'?';
|
var nm=w.name||(w.text||'').split('—')[0].trim()||w.doc_id||'?';
|
||||||
var initials=nm.split(' ').map(function(s){return (s[0]||'').toUpperCase()}).join('').substring(0,2);
|
|
||||||
row.appendChild(el('div','av',initials));
|
|
||||||
var info=el('div','info');
|
|
||||||
var n=el('div','nm',nm);
|
|
||||||
if((w.playbook_boost||0)>0){
|
|
||||||
n.appendChild(el('span','boost-chip','Endorsed · '+((w.playbook_citations||[]).length||'?')+' past fill(s)'));
|
|
||||||
}
|
|
||||||
info.appendChild(n);
|
|
||||||
var bits=[];
|
var bits=[];
|
||||||
if(w.role) bits.push(w.role);
|
if(w.role) bits.push(w.role);
|
||||||
if(w.city&&w.state) bits.push(w.city+', '+w.state);
|
if(w.city&&w.state) bits.push(w.city+', '+w.state);
|
||||||
if(w.rel!==undefined) bits.push('reliability '+Math.round(w.rel*100)+'%');
|
if(w.rel!==undefined) bits.push('reliability '+Math.round(w.rel*100)+'%');
|
||||||
if(w.avail!==undefined) bits.push('availability '+Math.round(w.avail*100)+'%');
|
if(w.avail!==undefined) bits.push('availability '+Math.round(w.avail*100)+'%');
|
||||||
info.appendChild(el('div','why',bits.join(' · ')||'AI semantic match'));
|
var endorsed = (w.playbook_boost||0) > 0
|
||||||
row.appendChild(info);
|
? 'Endorsed · '+((w.playbook_citations||[]).length||'?')+' past fill(s)'
|
||||||
|
: null;
|
||||||
|
var row = workerRow(nm, w.role||'', bits.join(' · ')||'AI semantic match', { endorsed: endorsed });
|
||||||
row.appendChild(el('div','score','#'+(i+1)));
|
row.appendChild(el('div','score','#'+(i+1)));
|
||||||
card.appendChild(row);
|
card.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|||||||
606
mcp-server/contractor.html
Normal file
606
mcp-server/contractor.html
Normal file
@ -0,0 +1,606 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head>
|
||||||
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Contractor Profile · Staffing Co-Pilot</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html,body{overflow-x:hidden}
|
||||||
|
body{font-family:'Inter',-apple-system,system-ui,sans-serif;background:#090c10;color:#b0b8c4;font-size:14px;line-height:1.6}
|
||||||
|
.bar{background:#0d1117;padding:0 24px;height:56px;border-bottom:1px solid #171d27;display:flex;justify-content:space-between;align-items:center}
|
||||||
|
.bar h1{font-size:14px;font-weight:600;color:#e6edf3}
|
||||||
|
.bar a{color:#545d68;text-decoration:none;font-size:12px;padding:6px 14px;border-radius:6px}
|
||||||
|
.bar a:hover{color:#e6edf3;background:#161b22}
|
||||||
|
.content{max-width:1100px;margin:0 auto;padding:24px 20px 40px}
|
||||||
|
.search-box{background:#0d1117;border:1px solid #21262d;border-radius:10px;padding:16px;margin-bottom:24px;display:flex;gap:10px}
|
||||||
|
.search-box input{flex:1;padding:12px 16px;background:#161b22;border:1px solid #21262d;border-radius:8px;color:#e6edf3;font-size:14px;outline:none}
|
||||||
|
.search-box input:focus{border-color:#388bfd}
|
||||||
|
.search-box button{padding:12px 24px;background:#1f6feb;border:none;border-radius:8px;color:#fff;font-weight:600;cursor:pointer}
|
||||||
|
.hero{background:#0d1117;border:1px solid #171d27;border-radius:12px;padding:24px;margin-bottom:16px}
|
||||||
|
.hero h2{color:#e6edf3;font-size:22px;font-weight:700;letter-spacing:-0.5px;margin-bottom:6px}
|
||||||
|
.hero .ticker-row{display:flex;align-items:center;gap:10px;margin-top:10px;flex-wrap:wrap}
|
||||||
|
.hero .ticker{font-family:ui-monospace,SFMono-Regular,monospace;background:#161b22;padding:4px 10px;border-radius:6px;color:#3fb950;border:1px solid #3fb95066;font-weight:600;font-size:12px}
|
||||||
|
.hero .meta{font-size:12px;color:#8b949e}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:14px}
|
||||||
|
.card{background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px}
|
||||||
|
.card h3{font-size:11px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;margin-bottom:10px;font-weight:600}
|
||||||
|
.card .big{font-size:24px;font-weight:700;color:#e6edf3;letter-spacing:-0.5px;margin-bottom:4px}
|
||||||
|
.card .sub{font-size:11px;color:#8b949e;line-height:1.5}
|
||||||
|
.card a{color:#58a6ff;text-decoration:none;font-size:11px}
|
||||||
|
.row{display:flex;justify-content:space-between;align-items:baseline;padding:6px 0;border-bottom:1px dashed #1f2631;font-size:11px}
|
||||||
|
.row:last-child{border:none}
|
||||||
|
.row .l{color:#8b949e}
|
||||||
|
.row .v{color:#e6edf3;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}
|
||||||
|
.chip{display:inline-block;padding:3px 8px;border-radius:9px;font-size:10px;font-weight:600;margin-right:6px;margin-bottom:4px}
|
||||||
|
.ld{color:#3d444d;text-align:center;padding:60px;font-size:13px}
|
||||||
|
.empty{color:#545d68;font-size:11px;font-style:italic;line-height:1.5}
|
||||||
|
.wide{grid-column:1/-1}
|
||||||
|
.heatmap{height:380px;border-radius:8px;border:1px solid #1f2631;overflow:hidden;margin-top:10px}
|
||||||
|
.heatmap .leaflet-container{background:#0a0d12}
|
||||||
|
.timeline{margin-top:10px;display:flex;align-items:flex-end;gap:2px;height:80px;padding:6px 0;border-bottom:1px solid #1f2631}
|
||||||
|
.timeline .tbar{flex:1;background:#1f6feb;min-height:2px;border-radius:2px 2px 0 0;position:relative;cursor:help}
|
||||||
|
.timeline .tbar:hover{background:#58a6ff}
|
||||||
|
.timeline-axis{display:flex;justify-content:space-between;font-size:10px;color:#545d68;padding-top:4px;font-family:ui-monospace,monospace}
|
||||||
|
.placeholder-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px;margin-top:14px}
|
||||||
|
.ph-card{background:#0a0d12;border:1px dashed #21262d;border-radius:8px;padding:12px 14px;position:relative}
|
||||||
|
.ph-card h4{font-size:11px;color:#8b949e;font-weight:600;margin-bottom:4px;display:flex;align-items:center;gap:6px}
|
||||||
|
.ph-card h4 .badge{font-size:9px;padding:2px 6px;border-radius:8px;background:#161b22;color:#d29922;border:1px solid #d2992244;font-weight:600;letter-spacing:0.5px;text-transform:uppercase}
|
||||||
|
.ph-card .why{font-size:11px;color:#e6edf3;line-height:1.5;margin-bottom:6px}
|
||||||
|
.ph-card .would{font-size:10px;color:#545d68;font-family:ui-monospace,monospace;line-height:1.5;border-top:1px dashed #1f2631;padding-top:6px;margin-top:6px}
|
||||||
|
.section-label{font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.4px;font-weight:600;margin:24px 0 8px}
|
||||||
|
@media(max-width:640px){.bar{padding:0 14px}.content{padding:14px}.hero{padding:16px}.hero h2{font-size:18px}.card{padding:12px}}
|
||||||
|
</style>
|
||||||
|
</head><body>
|
||||||
|
<div class="bar">
|
||||||
|
<h1>Staffing Co-Pilot · Contractor Profile</h1>
|
||||||
|
<a href="/">← Dashboard</a>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="search-box">
|
||||||
|
<input id="q" type="text" placeholder="Type a contractor name (e.g., Turner Construction Company)" onkeydown="if(event.key==='Enter')lookup()">
|
||||||
|
<button onclick="lookup()">Look up</button>
|
||||||
|
</div>
|
||||||
|
<div id="out"><div class="ld">Type a name above to load the full portfolio across every wired data source.</div></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function $(id){return document.getElementById(id)}
|
||||||
|
|
||||||
|
// Path prefix detection — devop.live serves this page under /lakehouse,
|
||||||
|
// localhost:3700 serves it at root. URL rewrites must preserve whatever
|
||||||
|
// prefix the user reached the page through, otherwise the back-link and
|
||||||
|
// browser refresh break.
|
||||||
|
var P=location.pathname.indexOf('/lakehouse')>=0?'/lakehouse':'';
|
||||||
|
|
||||||
|
// Bootstrap from URL: /contractor?name=Turner+Construction
|
||||||
|
window.addEventListener('load', function(){
|
||||||
|
var name = new URLSearchParams(location.search).get('name');
|
||||||
|
if(name){
|
||||||
|
$('q').value = name;
|
||||||
|
lookup();
|
||||||
|
}
|
||||||
|
// Back link respects the prefix too
|
||||||
|
var back=document.querySelector('.bar a');
|
||||||
|
if(back) back.href=P+'/';
|
||||||
|
});
|
||||||
|
|
||||||
|
function lookup(){
|
||||||
|
var name = $('q').value.trim();
|
||||||
|
if(!name){ $('out').textContent = ''; return; }
|
||||||
|
history.replaceState({}, '', P+'/contractor?name='+encodeURIComponent(name));
|
||||||
|
var out = $('out');
|
||||||
|
out.textContent = '';
|
||||||
|
var ld = document.createElement('div');
|
||||||
|
ld.className = 'ld';
|
||||||
|
ld.textContent = 'Pulling OSHA, SEC, Stooq, Chicago history, USASpending… (~5-10s on cold cache)';
|
||||||
|
out.appendChild(ld);
|
||||||
|
fetch(P+'/intelligence/contractor_profile',{
|
||||||
|
method:'POST',
|
||||||
|
headers:{'Content-Type':'application/json'},
|
||||||
|
body:JSON.stringify({name:name})
|
||||||
|
}).then(function(r){return r.json()}).then(function(d){
|
||||||
|
render(d);
|
||||||
|
}).catch(function(e){
|
||||||
|
out.textContent = '';
|
||||||
|
var err = document.createElement('div');
|
||||||
|
err.className = 'ld';
|
||||||
|
err.style.color = '#f85149';
|
||||||
|
err.textContent = 'profile failed: '+e.message;
|
||||||
|
out.appendChild(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(d){
|
||||||
|
var out = $('out');
|
||||||
|
out.textContent = '';
|
||||||
|
|
||||||
|
// ─── Hero — name, ticker, parent ─────────────
|
||||||
|
var hero = document.createElement('div');
|
||||||
|
hero.className = 'hero';
|
||||||
|
var h2 = document.createElement('h2');
|
||||||
|
h2.textContent = d.display_name;
|
||||||
|
hero.appendChild(h2);
|
||||||
|
var sub = document.createElement('div');
|
||||||
|
sub.className = 'meta';
|
||||||
|
sub.textContent = 'Internal ticker: '+(d.ticker||'?')+' · profile generated '+new Date(d.generated_at).toLocaleTimeString();
|
||||||
|
hero.appendChild(sub);
|
||||||
|
|
||||||
|
var trow = document.createElement('div');
|
||||||
|
trow.className = 'ticker-row';
|
||||||
|
// Direct ticker
|
||||||
|
var s = d.stock;
|
||||||
|
if(s && s.status==='ok'){
|
||||||
|
var tk = document.createElement('span');
|
||||||
|
tk.className = 'ticker';
|
||||||
|
tk.textContent = s.ticker;
|
||||||
|
trow.appendChild(tk);
|
||||||
|
var px = document.createElement('span');
|
||||||
|
px.className = 'meta';
|
||||||
|
px.textContent = (s.company_name||'')+(s.exchange?' · '+s.exchange:'')+(s.price?' · $'+s.price.toFixed(2):'');
|
||||||
|
if(s.day_change_pct!=null && !isNaN(s.day_change_pct)){
|
||||||
|
var ch = (s.day_change_pct>=0?'+':'')+s.day_change_pct.toFixed(2)+'%';
|
||||||
|
var chSpan = document.createElement('span');
|
||||||
|
chSpan.style.color = s.day_change_pct>=0?'#3fb950':'#f85149';
|
||||||
|
chSpan.style.marginLeft = '6px';
|
||||||
|
chSpan.textContent = ch;
|
||||||
|
px.appendChild(chSpan);
|
||||||
|
}
|
||||||
|
trow.appendChild(px);
|
||||||
|
} else {
|
||||||
|
var noTk = document.createElement('span');
|
||||||
|
noTk.className = 'meta';
|
||||||
|
noTk.textContent = 'Private — no direct US ticker';
|
||||||
|
trow.appendChild(noTk);
|
||||||
|
}
|
||||||
|
// Parent link
|
||||||
|
var pl = d.parent_link;
|
||||||
|
if(pl && pl.status==='ok'){
|
||||||
|
var arrow = document.createElement('span');
|
||||||
|
arrow.className = 'meta';
|
||||||
|
arrow.style.color = '#545d68';
|
||||||
|
arrow.textContent = ' → parent ';
|
||||||
|
trow.appendChild(arrow);
|
||||||
|
var pTk = document.createElement('span');
|
||||||
|
pTk.className = 'ticker';
|
||||||
|
pTk.style.color = '#d29922';
|
||||||
|
pTk.style.borderColor = '#d2992266';
|
||||||
|
pTk.textContent = pl.parent_ticker || '?';
|
||||||
|
pTk.title = pl.link_source || '';
|
||||||
|
trow.appendChild(pTk);
|
||||||
|
var pName = document.createElement('span');
|
||||||
|
pName.className = 'meta';
|
||||||
|
pName.textContent = pl.parent_name+(pl.parent_exchange?' · '+pl.parent_exchange:'')+(pl.parent_country?' · '+pl.parent_country:'');
|
||||||
|
trow.appendChild(pName);
|
||||||
|
} else if(pl && pl.status==='no_link'){
|
||||||
|
var pp = document.createElement('span');
|
||||||
|
pp.className = 'meta';
|
||||||
|
pp.style.fontStyle = 'italic';
|
||||||
|
pp.textContent = ' · '+(pl.reason||'no public parent identified');
|
||||||
|
trow.appendChild(pp);
|
||||||
|
}
|
||||||
|
hero.appendChild(trow);
|
||||||
|
out.appendChild(hero);
|
||||||
|
|
||||||
|
// ─── Grid of cards ─────────────────────────────
|
||||||
|
var grid = document.createElement('div');
|
||||||
|
grid.className = 'grid';
|
||||||
|
|
||||||
|
// OSHA
|
||||||
|
var oCard = card('OSHA SAFETY HISTORY (NATIONAL)');
|
||||||
|
var osha = d.osha || {};
|
||||||
|
if(osha.status==='ok'){
|
||||||
|
big(oCard, osha.inspection_count + ' inspections', 'most recent '+(osha.most_recent_date||'?'));
|
||||||
|
rowEl(oCard, 'States seen', (osha.states_seen||[]).join(', ') || '?');
|
||||||
|
rowEl(oCard, 'Most recent', osha.most_recent_date||'?');
|
||||||
|
if(osha.recent_inspections && osha.recent_inspections.length){
|
||||||
|
var rep = document.createElement('div');
|
||||||
|
rep.style.marginTop = '8px';
|
||||||
|
rep.style.fontSize = '10px';
|
||||||
|
rep.style.color = '#545d68';
|
||||||
|
rep.textContent = 'Recent inspections:';
|
||||||
|
oCard.appendChild(rep);
|
||||||
|
osha.recent_inspections.slice(0,5).forEach(function(i){
|
||||||
|
var r = document.createElement('div');
|
||||||
|
r.style.fontSize = '10px';
|
||||||
|
r.style.color = '#8b949e';
|
||||||
|
r.style.fontFamily = 'ui-monospace,monospace';
|
||||||
|
r.style.padding = '2px 0';
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = i.detail_url;
|
||||||
|
a.target = '_blank';
|
||||||
|
a.textContent = i.id;
|
||||||
|
r.appendChild(a);
|
||||||
|
r.appendChild(document.createTextNode(' · '+i.date+' · '+i.state+' · '+i.type+' · '+i.scope));
|
||||||
|
oCard.appendChild(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if(osha.status==='no_match'){
|
||||||
|
big(oCard, 'No inspections', 'clean record');
|
||||||
|
} else {
|
||||||
|
empty(oCard, 'OSHA fetch error: '+(osha.error||'unknown'));
|
||||||
|
}
|
||||||
|
grid.appendChild(oCard);
|
||||||
|
|
||||||
|
// Chicago history
|
||||||
|
var hCard = card('CHICAGO PERMIT HISTORY (24mo + LIFETIME)');
|
||||||
|
var hist = d.history || {};
|
||||||
|
if(hist.status==='ok'){
|
||||||
|
big(hCard, hist.permits_historical_total+' permits all-time',
|
||||||
|
hist.permits_last_180d+' in last 180d · '+hist.permits_last_24mo+' in 24mo · trend: '+hist.trend);
|
||||||
|
rowEl(hCard, 'Cost (24mo)', hist.total_cost_last_24mo>=1e6 ? '$'+(hist.total_cost_last_24mo/1e6).toFixed(1)+'M' : '$'+Math.round(hist.total_cost_last_24mo/1e3)+'K');
|
||||||
|
if(hist.recent_permits && hist.recent_permits.length){
|
||||||
|
var rh = document.createElement('div');
|
||||||
|
rh.style.marginTop = '8px';
|
||||||
|
rh.style.fontSize = '10px';
|
||||||
|
rh.style.color = '#545d68';
|
||||||
|
rh.textContent = 'Recent Chicago permits:';
|
||||||
|
hCard.appendChild(rh);
|
||||||
|
hist.recent_permits.slice(0,5).forEach(function(p){
|
||||||
|
var r = document.createElement('div');
|
||||||
|
r.style.fontSize = '10px';
|
||||||
|
r.style.color = '#8b949e';
|
||||||
|
r.style.padding = '2px 0';
|
||||||
|
r.textContent = '· '+(p.date||'?')+' · '+p.work_type+' · $'+(p.cost||0).toLocaleString()+' · '+p.address;
|
||||||
|
hCard.appendChild(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
empty(hCard, 'Chicago history error');
|
||||||
|
}
|
||||||
|
grid.appendChild(hCard);
|
||||||
|
|
||||||
|
// Federal contracts
|
||||||
|
var fCard = card('FEDERAL CONTRACTS (USASpending.gov)');
|
||||||
|
var fed = d.federal || {};
|
||||||
|
if(fed.status==='ok' && fed.total_awards_count>0){
|
||||||
|
var dollars = fed.total_awards_value>=1e9 ? '$'+(fed.total_awards_value/1e9).toFixed(2)+'B'
|
||||||
|
: fed.total_awards_value>=1e6 ? '$'+(fed.total_awards_value/1e6).toFixed(1)+'M'
|
||||||
|
: '$'+Math.round(fed.total_awards_value/1e3)+'K';
|
||||||
|
big(fCard, dollars, fed.total_awards_count+' awards · most recent '+(fed.most_recent_award_date||'?'));
|
||||||
|
if(fed.top_agencies && fed.top_agencies.length){
|
||||||
|
var ta = document.createElement('div');
|
||||||
|
ta.style.marginTop = '6px';
|
||||||
|
ta.style.fontSize = '10px';
|
||||||
|
ta.style.color = '#545d68';
|
||||||
|
ta.textContent = 'Top awarding agencies:';
|
||||||
|
fCard.appendChild(ta);
|
||||||
|
fed.top_agencies.forEach(function(a){
|
||||||
|
var r = document.createElement('div');
|
||||||
|
r.style.fontSize = '11px';
|
||||||
|
r.style.color = '#8b949e';
|
||||||
|
r.style.padding = '3px 0';
|
||||||
|
var dollars2 = a.value>=1e6 ? '$'+(a.value/1e6).toFixed(1)+'M' : '$'+Math.round(a.value/1e3)+'K';
|
||||||
|
r.textContent = '· '+a.agency+' — '+dollars2;
|
||||||
|
fCard.appendChild(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(fed.source_url){
|
||||||
|
var lnk = document.createElement('a');
|
||||||
|
lnk.href = fed.source_url;
|
||||||
|
lnk.target = '_blank';
|
||||||
|
lnk.style.display = 'inline-block';
|
||||||
|
lnk.style.marginTop = '8px';
|
||||||
|
lnk.textContent = 'View on usaspending.gov ↗';
|
||||||
|
fCard.appendChild(lnk);
|
||||||
|
}
|
||||||
|
} else if(fed.status==='no_match'){
|
||||||
|
big(fCard, 'No federal contracts', 'on file under this name');
|
||||||
|
} else {
|
||||||
|
empty(fCard, 'usaspending error');
|
||||||
|
}
|
||||||
|
grid.appendChild(fCard);
|
||||||
|
|
||||||
|
// Debarment + NLRB combined
|
||||||
|
var rCard = card('DEBARMENT + LABOR ACTIONS');
|
||||||
|
var deb = d.debarment || {};
|
||||||
|
var nlrb = d.nlrb || {};
|
||||||
|
rowEl(rCard, 'SAM.gov excluded', deb.status==='needs_setup' ? 'awaiting API key' : (deb.sam_excluded?'YES':'no'));
|
||||||
|
rowEl(rCard, 'IDOL debarred', deb.status==='needs_setup' ? 'awaiting scrape' : (deb.idol_debarred?'YES':'no'));
|
||||||
|
rowEl(rCard, 'NLRB cases', nlrb.status==='needs_setup' ? 'awaiting scrape' : (nlrb.total_cases||0));
|
||||||
|
if(deb.status==='needs_setup' || nlrb.status==='needs_setup'){
|
||||||
|
var dn = document.createElement('div');
|
||||||
|
dn.className = 'empty';
|
||||||
|
dn.style.marginTop = '8px';
|
||||||
|
dn.textContent = 'Both sources pending wire-up: '+(deb.reason||nlrb.reason||'');
|
||||||
|
rCard.appendChild(dn);
|
||||||
|
}
|
||||||
|
grid.appendChild(rCard);
|
||||||
|
|
||||||
|
// ILSOS
|
||||||
|
var iCard = card('CORPORATE REGISTRY (Illinois SoS)');
|
||||||
|
var ilsos = d.ilsos || {};
|
||||||
|
if(ilsos.status==='source_unreachable'){
|
||||||
|
rowEl(iCard, 'Status', 'source blocked at our ASN');
|
||||||
|
var en = document.createElement('div');
|
||||||
|
en.className = 'empty';
|
||||||
|
en.style.marginTop = '8px';
|
||||||
|
en.textContent = ilsos.reason||'';
|
||||||
|
iCard.appendChild(en);
|
||||||
|
} else if(ilsos.status==='ok'){
|
||||||
|
rowEl(iCard, 'Entity name', ilsos.entity_name||'?');
|
||||||
|
rowEl(iCard, 'File #', ilsos.file_number||'?');
|
||||||
|
rowEl(iCard, 'Status', ilsos.status_text||'?');
|
||||||
|
rowEl(iCard, 'Formed', ilsos.formation_date||'?');
|
||||||
|
rowEl(iCard, 'Registered agent', ilsos.registered_agent||'?');
|
||||||
|
} else {
|
||||||
|
empty(iCard, 'no ILSOS data');
|
||||||
|
}
|
||||||
|
grid.appendChild(iCard);
|
||||||
|
|
||||||
|
out.appendChild(grid);
|
||||||
|
|
||||||
|
// ─── Project Index summary — the staffer-facing build-signal score ──
|
||||||
|
var pixHeader = document.createElement('div');
|
||||||
|
pixHeader.className = 'section-label';
|
||||||
|
pixHeader.textContent = '◆ Project Index — build-signal score';
|
||||||
|
out.appendChild(pixHeader);
|
||||||
|
|
||||||
|
var pixCard = document.createElement('div');
|
||||||
|
pixCard.className = 'card wide';
|
||||||
|
// Score is a simple weighted blend of the wired signals — designed to
|
||||||
|
// be replaced with a real model once enough placeholders are wired.
|
||||||
|
var hist2 = d.history || {};
|
||||||
|
var pixScore = 0;
|
||||||
|
var pixDrivers = [];
|
||||||
|
if(hist2.permits_last_180d){ pixScore += Math.min(hist2.permits_last_180d * 5, 30); pixDrivers.push(hist2.permits_last_180d+' Chicago permits in 180d (+'+Math.min(hist2.permits_last_180d*5,30)+')'); }
|
||||||
|
if(hist2.trend === 'rising'){ pixScore += 10; pixDrivers.push('permit trend rising (+10)'); }
|
||||||
|
if(d.osha && d.osha.status==='ok' && d.osha.inspection_count>0){ pixScore -= Math.min(d.osha.inspection_count*5, 25); pixDrivers.push(d.osha.inspection_count+' OSHA inspections (-'+Math.min(d.osha.inspection_count*5,25)+')'); }
|
||||||
|
if(d.federal && d.federal.status==='ok' && d.federal.total_awards_count>0){ pixScore += 15; pixDrivers.push('federally-vetted contractor (+15)'); }
|
||||||
|
if(d.debarment && d.debarment.sam_excluded){ pixScore -= 50; pixDrivers.push('SAM.gov excluded (-50)'); }
|
||||||
|
if(d.stock && d.stock.status==='ok'){ pixScore += 5; pixDrivers.push('public ticker (+5)'); }
|
||||||
|
pixScore = Math.max(0, Math.min(100, 50 + pixScore));
|
||||||
|
var pixColor = pixScore >= 70 ? '#3fb950' : pixScore >= 40 ? '#d29922' : '#f85149';
|
||||||
|
var pixHero = document.createElement('div');
|
||||||
|
pixHero.style.cssText = 'display:flex;align-items:baseline;gap:14px;margin-bottom:8px';
|
||||||
|
var pixBig = document.createElement('span');
|
||||||
|
pixBig.style.cssText = 'font-size:42px;font-weight:700;color:'+pixColor+';letter-spacing:-1px';
|
||||||
|
pixBig.textContent = pixScore;
|
||||||
|
pixHero.appendChild(pixBig);
|
||||||
|
var pixLabel = document.createElement('span');
|
||||||
|
pixLabel.style.cssText = 'font-size:12px;color:#8b949e';
|
||||||
|
pixLabel.textContent = pixScore >= 70 ? 'Strong staffing partner — wired signals positive' : pixScore >= 40 ? 'Mixed signals — review drivers below' : 'Caution — wired signals negative';
|
||||||
|
pixHero.appendChild(pixLabel);
|
||||||
|
pixCard.appendChild(pixHero);
|
||||||
|
if(pixDrivers.length){
|
||||||
|
var pixDrv = document.createElement('div');
|
||||||
|
pixDrv.style.cssText = 'font-size:11px;color:#8b949e;line-height:1.7;font-family:ui-monospace,monospace';
|
||||||
|
pixDrv.textContent = pixDrivers.join(' · ');
|
||||||
|
pixCard.appendChild(pixDrv);
|
||||||
|
}
|
||||||
|
var pixFoot = document.createElement('div');
|
||||||
|
pixFoot.style.cssText = 'font-size:10px;color:#545d68;margin-top:8px;font-style:italic;line-height:1.5';
|
||||||
|
pixFoot.textContent = 'Score is a placeholder weighted blend of the 6 wired signals above. Real ML model lands once 12 awaiting sources below ship — that gives the index 18 features instead of 6.';
|
||||||
|
pixCard.appendChild(pixFoot);
|
||||||
|
out.appendChild(pixCard);
|
||||||
|
|
||||||
|
// ─── Heat map — every Chicago permit they're contact_1 or contact_2 on ─
|
||||||
|
var mapHeader = document.createElement('div');
|
||||||
|
mapHeader.className = 'section-label';
|
||||||
|
mapHeader.textContent = '◆ Where they\'ve worked — Chicago permits, last 24 months';
|
||||||
|
out.appendChild(mapHeader);
|
||||||
|
var mapCard = document.createElement('div');
|
||||||
|
mapCard.className = 'card wide';
|
||||||
|
var mapDiv = document.createElement('div');
|
||||||
|
mapDiv.className = 'heatmap';
|
||||||
|
mapDiv.id = 'cmap';
|
||||||
|
mapCard.appendChild(mapDiv);
|
||||||
|
var mapHint = document.createElement('div');
|
||||||
|
mapHint.style.cssText = 'font-size:11px;color:#545d68;margin-top:8px';
|
||||||
|
mapHint.textContent = 'Loading geo from chicago_permits…';
|
||||||
|
mapCard.appendChild(mapHint);
|
||||||
|
out.appendChild(mapCard);
|
||||||
|
|
||||||
|
// Plot the recent_permits embedded in the contractor profile (now
|
||||||
|
// includes lat/lng/permit_id/description per the entity.ts change).
|
||||||
|
// Color by cost: green <$100K, amber $100K-$1M, red ≥$1M.
|
||||||
|
var permits = (hist2.recent_permits||[]).filter(function(p){return p.lat&&p.lng});
|
||||||
|
if(!permits.length){
|
||||||
|
mapHint.textContent = 'No geocoded permits in the contractor history (Socrata may not have lat/lng for these records).';
|
||||||
|
} else {
|
||||||
|
// Construct map only after the div is in the DOM; defer one tick.
|
||||||
|
setTimeout(function(){
|
||||||
|
var map = L.map('cmap', {zoomControl:true, attributionControl:false}).setView([41.88,-87.63], 11);
|
||||||
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{maxZoom:19}).addTo(map);
|
||||||
|
var bounds = [];
|
||||||
|
var costs = permits.map(function(p){return Number(p.cost)||0});
|
||||||
|
var maxCost = Math.max.apply(null, costs.concat([1]));
|
||||||
|
permits.forEach(function(p){
|
||||||
|
var c = Number(p.cost)||0;
|
||||||
|
var radius = 4 + (c/maxCost)*16;
|
||||||
|
var color = c >= 1000000 ? '#f85149' : c >= 100000 ? '#d29922' : '#3fb950';
|
||||||
|
var marker = L.circleMarker([p.lat,p.lng],{radius:radius, color:color, weight:1, fillOpacity:0.55});
|
||||||
|
// Build popup via DOM (no innerHTML — keeps the XSS hook happy)
|
||||||
|
var pop = document.createElement('div');
|
||||||
|
pop.style.cssText = 'font-family:ui-monospace,monospace;font-size:11px;color:#0a0d12;min-width:160px';
|
||||||
|
var costRow = document.createElement('div');
|
||||||
|
costRow.style.cssText = 'font-weight:700;margin-bottom:4px';
|
||||||
|
costRow.textContent = '$'+c.toLocaleString()+' · '+(p.date||'?');
|
||||||
|
pop.appendChild(costRow);
|
||||||
|
var wt = document.createElement('div');
|
||||||
|
wt.textContent = p.work_type||'?';
|
||||||
|
pop.appendChild(wt);
|
||||||
|
var addr = document.createElement('div');
|
||||||
|
addr.style.color = '#545d68';
|
||||||
|
addr.textContent = p.address||'?';
|
||||||
|
pop.appendChild(addr);
|
||||||
|
if(p.permit_id){
|
||||||
|
var pid = document.createElement('div');
|
||||||
|
pid.style.cssText = 'color:#545d68;margin-top:4px;font-size:10px';
|
||||||
|
pid.textContent = 'permit '+p.permit_id;
|
||||||
|
pop.appendChild(pid);
|
||||||
|
}
|
||||||
|
marker.bindPopup(pop);
|
||||||
|
marker.addTo(map);
|
||||||
|
bounds.push([p.lat, p.lng]);
|
||||||
|
});
|
||||||
|
if(bounds.length>1) map.fitBounds(bounds, {padding:[24,24]});
|
||||||
|
mapHint.textContent = permits.length+' permits plotted · green <$100K, amber $100K-$1M, red ≥$1M · radius: relative cost';
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── History timeline — monthly permit volume + cost trend ─────────
|
||||||
|
if(hist2.recent_permits && hist2.recent_permits.length){
|
||||||
|
var tlHeader = document.createElement('div');
|
||||||
|
tlHeader.className = 'section-label';
|
||||||
|
tlHeader.textContent = '◆ Activity timeline — Chicago permits by month';
|
||||||
|
out.appendChild(tlHeader);
|
||||||
|
var tlCard = document.createElement('div');
|
||||||
|
tlCard.className = 'card wide';
|
||||||
|
// Bucket by year-month
|
||||||
|
var buckets = {};
|
||||||
|
hist2.recent_permits.forEach(function(p){
|
||||||
|
var d = (p.date||'').substring(0,7); // YYYY-MM
|
||||||
|
if(!d) return;
|
||||||
|
buckets[d] = buckets[d] || {count:0, cost:0};
|
||||||
|
buckets[d].count++;
|
||||||
|
buckets[d].cost += Number(p.cost)||0;
|
||||||
|
});
|
||||||
|
var months = Object.keys(buckets).sort();
|
||||||
|
if(months.length){
|
||||||
|
var maxC = Math.max.apply(null, months.map(function(m){return buckets[m].count}));
|
||||||
|
var tl = document.createElement('div'); tl.className='timeline';
|
||||||
|
months.forEach(function(m){
|
||||||
|
var b = buckets[m];
|
||||||
|
var bar = document.createElement('div'); bar.className='tbar';
|
||||||
|
bar.style.height = Math.max(2, Math.round(b.count/maxC*72)) + 'px';
|
||||||
|
bar.title = m+' · '+b.count+' permit'+(b.count===1?'':'s')+' · $'+Math.round(b.cost).toLocaleString();
|
||||||
|
tl.appendChild(bar);
|
||||||
|
});
|
||||||
|
tlCard.appendChild(tl);
|
||||||
|
var ax = document.createElement('div'); ax.className='timeline-axis';
|
||||||
|
var first = document.createElement('span'); first.textContent = months[0];
|
||||||
|
var last = document.createElement('span'); last.textContent = months[months.length-1];
|
||||||
|
ax.appendChild(first); ax.appendChild(last);
|
||||||
|
tlCard.appendChild(ax);
|
||||||
|
}
|
||||||
|
out.appendChild(tlCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 12 awaiting-source placeholders ──────────────────────────────
|
||||||
|
// Each one names a real public data source that would feed the
|
||||||
|
// build-signal index, with a one-line "why a staffer cares" framing
|
||||||
|
// and a sample shape of what the panel would show once wired.
|
||||||
|
var phHeader = document.createElement('div');
|
||||||
|
phHeader.className = 'section-label';
|
||||||
|
phHeader.textContent = '◆ 12 awaiting sources — what plugs in next';
|
||||||
|
out.appendChild(phHeader);
|
||||||
|
var phGrid = document.createElement('div');
|
||||||
|
phGrid.className = 'placeholder-grid';
|
||||||
|
PLACEHOLDERS.forEach(function(p){
|
||||||
|
var c = document.createElement('div'); c.className='ph-card';
|
||||||
|
var h = document.createElement('h4');
|
||||||
|
var name = document.createElement('span'); name.textContent = p.name;
|
||||||
|
var badge = document.createElement('span'); badge.className='badge'; badge.textContent='AWAITING';
|
||||||
|
h.appendChild(name); h.appendChild(badge);
|
||||||
|
c.appendChild(h);
|
||||||
|
var why = document.createElement('div'); why.className='why'; why.textContent = p.why;
|
||||||
|
c.appendChild(why);
|
||||||
|
var would = document.createElement('div'); would.className='would';
|
||||||
|
would.textContent = 'Would show: ' + p.would;
|
||||||
|
c.appendChild(would);
|
||||||
|
phGrid.appendChild(c);
|
||||||
|
});
|
||||||
|
out.appendChild(phGrid);
|
||||||
|
|
||||||
|
// Roadmap footer
|
||||||
|
var foot = document.createElement('div');
|
||||||
|
foot.style.marginTop = '20px';
|
||||||
|
foot.style.fontSize = '10px';
|
||||||
|
foot.style.color = '#484f58';
|
||||||
|
foot.style.lineHeight = '1.6';
|
||||||
|
foot.textContent = 'Wired: OSHA Enforcement · SEC EDGAR + Stooq · Chicago Socrata permits (lat/lng) · USASpending.gov · curated parent-ticker map · ILSOS (datacenter ASN blocked). 12 awaiting sources above are real public datasets that would 3× the feature count of the build-signal index — each one labeled with the one-liner the staffer would ask before placing a worker.';
|
||||||
|
out.appendChild(foot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twelve real public data sources, framed in coordinator language.
|
||||||
|
// Each is a placeholder; the panel renders them as "AWAITING" with a
|
||||||
|
// description of what they'd add once wired. Order is roughly: highest
|
||||||
|
// staffing-decision relevance first.
|
||||||
|
var PLACEHOLDERS = [
|
||||||
|
{
|
||||||
|
name: 'DOL Wage & Hour (WHD)',
|
||||||
|
why: 'Has this contractor stiffed workers before? WHD posts every back-wage settlement and unpaid-overtime case.',
|
||||||
|
would: 'cases last 24mo · total back wages owed · status by state · most recent settlement date · whether the workers got paid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'State Licensure Boards',
|
||||||
|
why: 'Is the contractor legally allowed to do this work today, in this state?',
|
||||||
|
would: 'license # · status (active / expired / suspended) · trade scope · expiration date · disciplinary history',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Surety Bond Capacity',
|
||||||
|
why: 'How big a job can this contractor actually take? Bond ceiling = upper bound on what they\'re bonded for.',
|
||||||
|
would: 'bonding company · single-contract ceiling · aggregate cap · current utilization · recent bond denials',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EPA ECHO Compliance',
|
||||||
|
why: 'If a worker shows up to a site with hazmat issues, that\'s the staffing company\'s problem too.',
|
||||||
|
would: 'facility-level violations · last enforcement action · pollutants · whether OSHA escalated',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DOT/FMCSA Carrier Safety',
|
||||||
|
why: 'For warehouses with on-site driving or carriers we cross-staff: crash rate, driver out-of-service rate, IFTA filings.',
|
||||||
|
would: 'crash rate per million miles · driver OOS % · vehicle OOS % · safety rating · last compliance review',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'BBB Complaints + Rating',
|
||||||
|
why: 'What do this contractor\'s own employees say happens to them? BBB aggregates complaints from workers and clients.',
|
||||||
|
would: 'rating · complaint count last 36mo · complaint categories (pay, safety, ghosted) · response rate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'PACER Civil Suits (Federal)',
|
||||||
|
why: 'Are they being sued for FLSA, discrimination, or wrongful termination? Filings predate enforcement actions.',
|
||||||
|
would: 'open suits · FLSA / Title VII / ADA breakdowns · counterparties · year-over-year filing rate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'UCC Lien Filings',
|
||||||
|
why: 'When a contractor stops paying suppliers, mechanics liens hit the public record. Cash-flow distress signal.',
|
||||||
|
would: 'open liens · total face value · filers (suppliers, banks) · last filing · whether resolved',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'D&B / Credit Bureau',
|
||||||
|
why: 'Will they pay our staffing invoices? D&B PAYDEX score is the standard.',
|
||||||
|
would: 'PAYDEX (1-100) · days-beyond-terms · credit limit recommendation · UCC link · trade payment trend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'State UI Employer Claims',
|
||||||
|
why: 'Workforce stability proxy. A spike in unemployment claims at this employer = layoffs or churn we should know about.',
|
||||||
|
would: 'claims filed against this employer last 12mo · approval rate · separation-reason breakdown',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MSHA Mine Safety',
|
||||||
|
why: 'For excavation, demolition, materials, aggregate — MSHA owns the citation history.',
|
||||||
|
would: 'citations · S&S violations · most recent fatality / serious injury · pattern-of-violation flag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Registered Apprenticeships (DOL RAPIDS)',
|
||||||
|
why: 'A contractor with active apprenticeship programs has built a workforce pipeline — different staffing partnership story than one without.',
|
||||||
|
would: 'active programs · apprentice count · trades covered · graduation rate · ethnic/gender diversity reported',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function card(title){
|
||||||
|
var c = document.createElement('div');
|
||||||
|
c.className = 'card';
|
||||||
|
var h = document.createElement('h3');
|
||||||
|
h.textContent = title;
|
||||||
|
c.appendChild(h);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
function big(c, value, sub){
|
||||||
|
var b = document.createElement('div'); b.className='big'; b.textContent=value;
|
||||||
|
var s = document.createElement('div'); s.className='sub'; s.textContent=sub;
|
||||||
|
c.appendChild(b); c.appendChild(s);
|
||||||
|
}
|
||||||
|
function rowEl(c, label, value){
|
||||||
|
var r = document.createElement('div'); r.className='row';
|
||||||
|
var l = document.createElement('span'); l.className='l'; l.textContent=label;
|
||||||
|
var v = document.createElement('span'); v.className='v'; v.textContent=value||'—';
|
||||||
|
r.appendChild(l); r.appendChild(v); c.appendChild(r);
|
||||||
|
}
|
||||||
|
function empty(c, msg){
|
||||||
|
var e = document.createElement('div'); e.className='empty'; e.textContent=msg;
|
||||||
|
c.appendChild(e);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body></html>
|
||||||
2821
mcp-server/entity.ts
Normal file
2821
mcp-server/entity.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,58 @@ import { buildPermitBrief } from "./entity.js";
|
|||||||
|
|
||||||
const BASE = process.env.LAKEHOUSE_URL || "http://localhost:3100";
|
const BASE = process.env.LAKEHOUSE_URL || "http://localhost:3100";
|
||||||
const PORT = parseInt(process.env.MCP_PORT || "3700");
|
const PORT = parseInt(process.env.MCP_PORT || "3700");
|
||||||
|
|
||||||
|
// ─── Staffer roster — used by the per-staffer hot-swap index (G). ────
|
||||||
|
//
|
||||||
|
// J's vision: each staffer has their own molded view of the corpus.
|
||||||
|
// When Maria searches, the system surfaces *Maria's* prior fills and
|
||||||
|
// her territory's playbooks first. When Aisha searches, the same
|
||||||
|
// corpus gets re-shaped to her geo and recent activity. This is what
|
||||||
|
// generic CRM fast-search can't do: a relevance gradient that
|
||||||
|
// compounds with each staffer's own signal.
|
||||||
|
//
|
||||||
|
// First implementation is geography-based — each staffer has a primary
|
||||||
|
// state and a list of cities they recruit for. Playbook queries get
|
||||||
|
// scoped to that territory when staffer_id is provided. As the system
|
||||||
|
// accumulates per-staffer signal (call_log assignments, email threads,
|
||||||
|
// SMS history), the scope expands beyond geography.
|
||||||
|
//
|
||||||
|
// Adding a staffer: append to this list. The /api/staffers endpoint
|
||||||
|
// exposes the public-safe fields to the UI dropdown.
|
||||||
|
const STAFFERS: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
display: string;
|
||||||
|
territory: { state: string; cities: string[] };
|
||||||
|
greeting: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
id: "maria",
|
||||||
|
name: "Maria",
|
||||||
|
display: "Maria · Chicago coordinator",
|
||||||
|
territory: { state: "IL", cities: ["Chicago", "Joliet", "Rockford", "Peoria", "Springfield", "Decatur"] },
|
||||||
|
greeting: "Maria's territory: Illinois warehouse + manufacturing fills",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "devon",
|
||||||
|
name: "Devon",
|
||||||
|
display: "Devon · Indiana coordinator",
|
||||||
|
territory: { state: "IN", cities: ["Indianapolis", "Fort Wayne", "South Bend", "Evansville", "Bloomington", "Terre Haute"] },
|
||||||
|
greeting: "Devon's territory: Indiana production + assembly fills",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aisha",
|
||||||
|
name: "Aisha",
|
||||||
|
display: "Aisha · Wisconsin/Michigan coordinator",
|
||||||
|
territory: { state: "WI", cities: ["Milwaukee", "Madison", "Green Bay", "Detroit", "Grand Rapids", "Lansing"] },
|
||||||
|
greeting: "Aisha's territory: Wisconsin + Michigan logistics",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function lookupStaffer(id: string | undefined): typeof STAFFERS[number] | null {
|
||||||
|
if (!id) return null;
|
||||||
|
return STAFFERS.find((s) => s.id === id) || null;
|
||||||
|
}
|
||||||
const MODE = process.env.MCP_TRANSPORT || "http"; // "stdio" or "http"
|
const MODE = process.env.MCP_TRANSPORT || "http"; // "stdio" or "http"
|
||||||
|
|
||||||
// Active trace for the current request — set per-request in the HTTP handler
|
// Active trace for the current request — set per-request in the HTTP handler
|
||||||
@ -824,6 +876,213 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch ticker quotes — used by the profiler page's scrolling
|
||||||
|
// ticker basket. Stooq's HTTP CSV API is single-symbol per call,
|
||||||
|
// so this fans out N tickers in parallel and returns a flat
|
||||||
|
// map. Non-US tickers (HOC.DE, SKA-B.ST, LLC.AX) won't have a
|
||||||
|
// Stooq.us entry; we surface that as null so the UI can render
|
||||||
|
// them with a "—" placeholder.
|
||||||
|
if (url.pathname === "/intelligence/ticker_quotes" && req.method === "POST") {
|
||||||
|
const start = Date.now();
|
||||||
|
const b = await json();
|
||||||
|
const tickers: string[] = Array.isArray(b.tickers) ? b.tickers.map((t: any) => String(t).toUpperCase()).filter(Boolean) : [];
|
||||||
|
if (!tickers.length) return ok({ quotes: {}, duration_ms: 0 });
|
||||||
|
const { fetchStooqQuote } = await import("./entity.js");
|
||||||
|
const dedup = Array.from(new Set(tickers)).slice(0, 50);
|
||||||
|
const results = await Promise.all(dedup.map(async (t) => {
|
||||||
|
// Skip non-US suffixes — Stooq.us won't have them
|
||||||
|
if (t.includes(".")) return [t, null] as const;
|
||||||
|
try {
|
||||||
|
const q = await fetchStooqQuote(t);
|
||||||
|
if (!q || !q.price) return [t, null] as const;
|
||||||
|
const change_pct = q.open && q.open > 0 ? ((q.price - q.open) / q.open) * 100 : null;
|
||||||
|
return [t, {
|
||||||
|
ticker: t,
|
||||||
|
price: q.price,
|
||||||
|
price_date: q.price_date,
|
||||||
|
open: q.open,
|
||||||
|
high: q.high,
|
||||||
|
low: q.low,
|
||||||
|
day_change_pct: change_pct,
|
||||||
|
stooq_url: `https://stooq.com/q/?s=${t.toLowerCase()}.us`,
|
||||||
|
}] as const;
|
||||||
|
} catch {
|
||||||
|
return [t, null] as const;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const quotes: Record<string, any> = {};
|
||||||
|
for (const [t, q] of results) quotes[t] = q;
|
||||||
|
return ok({ quotes, count: dedup.length, duration_ms: Date.now() - start });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profiler index — directory of every contractor that has filed
|
||||||
|
// a Chicago permit recently, ranked by permit count + total
|
||||||
|
// cost. Each name in the response links to the full /contractor
|
||||||
|
// profile page. Answers J's ask: "a profiler index that shows
|
||||||
|
// a history of everyone." Pulled live from Socrata; the
|
||||||
|
// count/cost aggregations let the staffer see who's actually
|
||||||
|
// active vs one-off LLCs.
|
||||||
|
if (url.pathname === "/intelligence/profiler_index" && req.method === "POST") {
|
||||||
|
const start = Date.now();
|
||||||
|
const b = await json();
|
||||||
|
const sinceDate = String(b.since || "2025-06-01");
|
||||||
|
const minCost = Math.max(0, Number(b.min_cost) || 100000);
|
||||||
|
const limit = Math.max(1, Math.min(500, Number(b.limit) || 200));
|
||||||
|
const search = String(b.search || "").trim();
|
||||||
|
const permitUrl = "https://data.cityofchicago.org/resource/ydr8-5enu.json";
|
||||||
|
// Group by contact_1_name AND by contact_2_name in two
|
||||||
|
// queries, then merge — Socrata's GROUP BY only takes one
|
||||||
|
// expression and we want both contractor slots.
|
||||||
|
const buildQuery = (col: string) => {
|
||||||
|
const where = [
|
||||||
|
`reported_cost>${minCost}`,
|
||||||
|
`issue_date>'${sinceDate.replace(/'/g, "")}'`,
|
||||||
|
`${col} IS NOT NULL`,
|
||||||
|
];
|
||||||
|
if (search) {
|
||||||
|
const s = search.replace(/'/g, "''").toUpperCase();
|
||||||
|
where.push(`upper(${col}) like '%${s}%'`);
|
||||||
|
}
|
||||||
|
return `${permitUrl}?$select=${col} AS name,count(*) as cnt,sum(reported_cost) as total_cost,max(issue_date) as last_filed&`
|
||||||
|
+ `$where=${encodeURIComponent(where.join(" AND "))}`
|
||||||
|
+ `&$group=${col}&$order=total_cost DESC&$limit=${limit}`;
|
||||||
|
};
|
||||||
|
// Co-occurrence query — pulls the contact_1+contact_2 pairs in
|
||||||
|
// the window so we can attribute tickers across associations
|
||||||
|
// ("Bob's Electric works for Target → show TGT"). Capped 5K
|
||||||
|
// permits, ~200ms cold; resolved tickers are in-memory after.
|
||||||
|
const buildCoQuery = () => {
|
||||||
|
const where = [
|
||||||
|
`reported_cost>${minCost}`,
|
||||||
|
`issue_date>'${sinceDate.replace(/'/g, "")}'`,
|
||||||
|
"contact_1_name IS NOT NULL",
|
||||||
|
"contact_2_name IS NOT NULL",
|
||||||
|
];
|
||||||
|
if (search) {
|
||||||
|
const s = search.replace(/'/g, "''").toUpperCase();
|
||||||
|
where.push(`(upper(contact_1_name) like '%${s}%' OR upper(contact_2_name) like '%${s}%')`);
|
||||||
|
}
|
||||||
|
return `${permitUrl}?$select=contact_1_name,contact_2_name`
|
||||||
|
+ `&$where=${encodeURIComponent(where.join(" AND "))}`
|
||||||
|
+ `&$limit=5000`;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const [byC1, byC2, coRows] = await Promise.all([
|
||||||
|
fetch(buildQuery("contact_1_name")).then((r) => r.json()).catch(() => []),
|
||||||
|
fetch(buildQuery("contact_2_name")).then((r) => r.json()).catch(() => []),
|
||||||
|
fetch(buildCoQuery()).then((r) => r.json()).catch(() => []),
|
||||||
|
]);
|
||||||
|
const merged: Record<string, { name: string; permits: number; total_cost: number; last_filed: string; roles: Set<string> }> = {};
|
||||||
|
const consume = (rows: any[], role: string) => {
|
||||||
|
for (const r of rows || []) {
|
||||||
|
const n = (r.name || "").trim();
|
||||||
|
if (!n) continue;
|
||||||
|
const cnt = parseInt(r.cnt, 10) || 0;
|
||||||
|
const cost = parseFloat(r.total_cost || "0") || 0;
|
||||||
|
const last = r.last_filed || "";
|
||||||
|
const key = n.toUpperCase();
|
||||||
|
if (!merged[key]) merged[key] = { name: n, permits: 0, total_cost: 0, last_filed: "", roles: new Set() };
|
||||||
|
merged[key].permits += cnt;
|
||||||
|
merged[key].total_cost += cost;
|
||||||
|
if (last > merged[key].last_filed) merged[key].last_filed = last;
|
||||||
|
merged[key].roles.add(role);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
consume(byC1, "applicant");
|
||||||
|
consume(byC2, "contractor");
|
||||||
|
|
||||||
|
// Build co-occurrence map from the permit pairs. For each
|
||||||
|
// contractor key, count how many permits they co-appeared
|
||||||
|
// on with each other party.
|
||||||
|
const coMap: Record<string, Record<string, number>> = {};
|
||||||
|
for (const r of (Array.isArray(coRows) ? coRows : []) as any[]) {
|
||||||
|
const a = String(r.contact_1_name || "").trim();
|
||||||
|
const b = String(r.contact_2_name || "").trim();
|
||||||
|
if (!a || !b) continue;
|
||||||
|
const ka = a.toUpperCase();
|
||||||
|
const kb = b.toUpperCase();
|
||||||
|
if (ka === kb) continue;
|
||||||
|
(coMap[ka] = coMap[ka] || {})[kb] = (coMap[ka][kb] || 0) + 1;
|
||||||
|
(coMap[kb] = coMap[kb] || {})[ka] = (coMap[kb][ka] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach tickers per contractor — direct, parent, and any
|
||||||
|
// tickers attributable to top co-occurring partners ("works
|
||||||
|
// with TARGET CORPORATION → TGT shown as associated"). Resolves
|
||||||
|
// via the in-memory SEC tickers index + curated parent map,
|
||||||
|
// so the cost is per-name index-lookup, not a network call.
|
||||||
|
const { lookupTickerLite } = await import("./entity.js");
|
||||||
|
// Memoize per-name to skip duplicate lookups across the
|
||||||
|
// associated step.
|
||||||
|
const tickerCache: Record<string, any[]> = {};
|
||||||
|
const lookupCached = async (n: string) => {
|
||||||
|
const k = n.toUpperCase();
|
||||||
|
if (tickerCache[k]) return tickerCache[k];
|
||||||
|
tickerCache[k] = await lookupTickerLite(n);
|
||||||
|
return tickerCache[k];
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowsBase = Object.values(merged)
|
||||||
|
.map((r) => ({ ...r, roles: Array.from(r.roles) }))
|
||||||
|
.sort((a, b) => b.total_cost - a.total_cost)
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
// Resolve tickers concurrently (in-memory ops, but Promise.all
|
||||||
|
// keeps the code uniform with future remote ticker sources).
|
||||||
|
const enriched = await Promise.all(rowsBase.map(async (r) => {
|
||||||
|
const direct = await lookupCached(r.name);
|
||||||
|
const partners = Object.entries(coMap[r.name.toUpperCase()] || {})
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 6);
|
||||||
|
const associated: any[] = [];
|
||||||
|
const seen = new Set(direct.map((t: any) => t.ticker));
|
||||||
|
for (const [partnerName, occurrences] of partners) {
|
||||||
|
const ts = await lookupCached(partnerName);
|
||||||
|
for (const t of ts) {
|
||||||
|
if (seen.has(t.ticker)) continue;
|
||||||
|
seen.add(t.ticker);
|
||||||
|
associated.push({
|
||||||
|
ticker: t.ticker,
|
||||||
|
via: "associated",
|
||||||
|
partner_name: partnerName,
|
||||||
|
co_permits: occurrences,
|
||||||
|
partner_via: t.via,
|
||||||
|
matched_name: t.matched_name,
|
||||||
|
});
|
||||||
|
if (associated.length >= 5) break;
|
||||||
|
}
|
||||||
|
if (associated.length >= 5) break;
|
||||||
|
}
|
||||||
|
return { ...r, tickers: { direct, associated } };
|
||||||
|
}));
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
count: enriched.length,
|
||||||
|
since: sinceDate,
|
||||||
|
min_cost: minCost,
|
||||||
|
search,
|
||||||
|
contractors: enriched,
|
||||||
|
duration_ms: Date.now() - start,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
return err(`profiler_index: ${e.message}`, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Staffer roster — read by the UI dropdown so each coordinator
|
||||||
|
// can act under their own identity (per-staffer hot-swap index).
|
||||||
|
if (url.pathname === "/api/staffers" || url.pathname === "/staffers") {
|
||||||
|
return ok({
|
||||||
|
staffers: STAFFERS.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
name: s.name,
|
||||||
|
display: s.display,
|
||||||
|
territory: s.territory,
|
||||||
|
greeting: s.greeting,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (url.pathname === "/system/summary") {
|
if (url.pathname === "/system/summary") {
|
||||||
const [ds, indexes, workersCount, candsCount] = await Promise.all([
|
const [ds, indexes, workersCount, candsCount] = await Promise.all([
|
||||||
api("GET", "/catalog/datasets").catch(() => [] as any),
|
api("GET", "/catalog/datasets").catch(() => [] as any),
|
||||||
@ -966,6 +1225,86 @@ async function main() {
|
|||||||
// OSHA national, Chicago history, ticker chart, parent link,
|
// OSHA national, Chicago history, ticker chart, parent link,
|
||||||
// federal contracts, debarment, unions, training. Click any
|
// federal contracts, debarment, unions, training. Click any
|
||||||
// contractor name in a permit Entity Brief to land here.
|
// contractor name in a permit Entity Brief to land here.
|
||||||
|
// Headshot pool — synthetic StyleGAN faces from
|
||||||
|
// thispersondoesnotexist.com fetched offline by
|
||||||
|
// scripts/staffing/fetch_face_pool.py. Deterministic mapping:
|
||||||
|
// hash(worker key) → pool index → image bytes. Same key always
|
||||||
|
// gets the same face; different keys spread evenly.
|
||||||
|
//
|
||||||
|
// Optional gender hint: ?g=man|woman narrows the pool to
|
||||||
|
// matching tagged faces (set by deepface during fetch). Falls
|
||||||
|
// back to whole pool if no matches.
|
||||||
|
if (url.pathname.startsWith("/headshots/") && req.method === "GET") {
|
||||||
|
const key = decodeURIComponent(url.pathname.slice("/headshots/".length));
|
||||||
|
const wantGender = url.searchParams.get("g") || "";
|
||||||
|
if (!key) return new Response("missing key", { status: 400 });
|
||||||
|
// Manifest is loaded lazily on first request and cached.
|
||||||
|
// Re-runs of the fetch script overwrite the manifest; the
|
||||||
|
// mcp-server can be poked to reload by hitting
|
||||||
|
// /headshots/__reload — the hash-key path will never have
|
||||||
|
// exactly two underscores so the collision risk is zero.
|
||||||
|
const HEADSHOT_DIR = "/home/profit/lakehouse/data/headshots";
|
||||||
|
if (key === "__reload" || !(globalThis as any)._faces) {
|
||||||
|
try {
|
||||||
|
const raw = await Bun.file(`${HEADSHOT_DIR}/manifest.jsonl`).text();
|
||||||
|
const lines = raw.trim().split("\n").filter(Boolean);
|
||||||
|
const all = lines.map((l) => JSON.parse(l));
|
||||||
|
(globalThis as any)._faces = {
|
||||||
|
all,
|
||||||
|
man: all.filter((r: any) => r.gender === "man"),
|
||||||
|
woman: all.filter((r: any) => r.gender === "woman"),
|
||||||
|
untagged: all.filter((r: any) => !r.gender || (r.gender !== "man" && r.gender !== "woman")),
|
||||||
|
loaded_at: Date.now(),
|
||||||
|
};
|
||||||
|
if (key === "__reload") {
|
||||||
|
return Response.json({
|
||||||
|
reloaded: true,
|
||||||
|
total: all.length,
|
||||||
|
man: (globalThis as any)._faces.man.length,
|
||||||
|
woman: (globalThis as any)._faces.woman.length,
|
||||||
|
untagged: (globalThis as any)._faces.untagged.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
return new Response(`face pool not available: ${e.message}. Run scripts/staffing/fetch_face_pool.py first.`, { status: 503 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const F = (globalThis as any)._faces as { all: any[]; man: any[]; woman: any[]; untagged: any[] };
|
||||||
|
if (!F || !F.all.length) {
|
||||||
|
return new Response("face pool empty", { status: 503 });
|
||||||
|
}
|
||||||
|
// Pool selection: gender hint > full pool. If no gender match,
|
||||||
|
// fall back to the full pool so the worker still gets a face.
|
||||||
|
let pool = F.all;
|
||||||
|
if (wantGender === "man" && F.man.length) pool = F.man;
|
||||||
|
else if (wantGender === "woman" && F.woman.length) pool = F.woman;
|
||||||
|
// Hash key → pool index. djb2-ish, fits any string.
|
||||||
|
let h = 5381;
|
||||||
|
for (let i = 0; i < key.length; i++) h = ((h << 5) + h + key.charCodeAt(i)) | 0;
|
||||||
|
const idx = Math.abs(h) % pool.length;
|
||||||
|
const pick = pool[idx];
|
||||||
|
const file = Bun.file(`${HEADSHOT_DIR}/${pick.file}`);
|
||||||
|
if (!(await file.exists())) {
|
||||||
|
return new Response("face missing on disk", { status: 404 });
|
||||||
|
}
|
||||||
|
return new Response(file, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "image/jpeg",
|
||||||
|
"Cache-Control": "public, max-age=86400, immutable",
|
||||||
|
"X-Face-Pool-Idx": String(pick.id),
|
||||||
|
"X-Face-Pool-Gender": pick.gender || "untagged",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profiler index — directory page of everyone who's filed a
|
||||||
|
// Chicago permit (clickable directory of contractors).
|
||||||
|
if (url.pathname === "/profiler" || url.pathname === "/contractors") {
|
||||||
|
return new Response(Bun.file(import.meta.dir + "/profiler.html"), {
|
||||||
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (url.pathname === "/contractor") {
|
if (url.pathname === "/contractor") {
|
||||||
return new Response(Bun.file(import.meta.dir + "/contractor.html"), {
|
return new Response(Bun.file(import.meta.dir + "/contractor.html"), {
|
||||||
headers: { ...cors, "Content-Type": "text/html" },
|
headers: { ...cors, "Content-Type": "text/html" },
|
||||||
@ -1285,6 +1624,7 @@ async function main() {
|
|||||||
if (url.pathname === "/intelligence/permit_contracts" && req.method === "POST") {
|
if (url.pathname === "/intelligence/permit_contracts" && req.method === "POST") {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
try {
|
try {
|
||||||
|
const b: any = await req.json().catch(() => ({}));
|
||||||
const permitUrl = "https://data.cityofchicago.org/resource/ydr8-5enu.json";
|
const permitUrl = "https://data.cityofchicago.org/resource/ydr8-5enu.json";
|
||||||
// Recent + substantial permits only — skip tiny ones that
|
// Recent + substantial permits only — skip tiny ones that
|
||||||
// don't imply real staffing demand.
|
// don't imply real staffing demand.
|
||||||
@ -1292,10 +1632,27 @@ async function main() {
|
|||||||
// panel on each card can populate without a second fetch.
|
// panel on each card can populate without a second fetch.
|
||||||
// Contacts identify the applicant / contractor by name —
|
// Contacts identify the applicant / contractor by name —
|
||||||
// those are the keys we pass to OSHA/ILSOS enrichment.
|
// those are the keys we pass to OSHA/ILSOS enrichment.
|
||||||
|
// Caller-controlled limit: J reported the live panel was
|
||||||
|
// dropping older permits (Target) because $limit=6 only ever
|
||||||
|
// showed today's 6 newest. Default 24 so a few days of
|
||||||
|
// permits stay on the panel; allow up to 100 via body.
|
||||||
|
const reqLimit = Math.max(1, Math.min(100, Number((b as any)?.limit) || 24));
|
||||||
|
// Optional contractor-name filter — lets the panel scope to
|
||||||
|
// a specific contact_1 or contact_2 name (e.g. "Target
|
||||||
|
// Corporation") so the user can pin a contractor to the panel
|
||||||
|
// without it scrolling past.
|
||||||
|
const cFilter = String((b as any)?.contractor || "").trim().replace(/'/g, "''");
|
||||||
|
const wherePieces: string[] = [
|
||||||
|
"reported_cost>250000",
|
||||||
|
"issue_date>'2025-06-01'",
|
||||||
|
];
|
||||||
|
if (cFilter) {
|
||||||
|
wherePieces.push(`(upper(contact_1_name)=upper('${cFilter}') OR upper(contact_2_name)=upper('${cFilter}'))`);
|
||||||
|
}
|
||||||
const permits: any[] = await fetch(
|
const permits: any[] = await fetch(
|
||||||
`${permitUrl}?$select=id,permit_type,work_type,work_description,reported_cost,street_number,street_direction,street_name,community_area,issue_date,contact_1_name,contact_1_type,contact_2_name,contact_2_type&`
|
`${permitUrl}?$select=id,permit_type,work_type,work_description,reported_cost,street_number,street_direction,street_name,community_area,issue_date,contact_1_name,contact_1_type,contact_2_name,contact_2_type,latitude,longitude&`
|
||||||
+ `$where=reported_cost>250000 AND issue_date>'2025-06-01'`
|
+ `$where=${encodeURIComponent(wherePieces.join(" AND "))}`
|
||||||
+ `&$order=issue_date DESC&$limit=6`
|
+ `&$order=issue_date DESC&$limit=${reqLimit}`
|
||||||
).then(r => r.json()).catch(() => []);
|
).then(r => r.json()).catch(() => []);
|
||||||
|
|
||||||
const typeToRole: Record<string, string> = {
|
const typeToRole: Record<string, string> = {
|
||||||
@ -1693,12 +2050,281 @@ async function main() {
|
|||||||
queries_run: queries, duration_ms: Date.now() - start });
|
queries_run: queries, duration_ms: Date.now() - start });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Route 6: late-worker / no-show triage. Coordinator gets a text
|
||||||
|
// ("Marcus running late site 4422") and needs three things in
|
||||||
|
// one shot: the worker's record + attendance pattern, a draft
|
||||||
|
// SMS to the client, and a ranked list of immediately-available
|
||||||
|
// backfills filtered by the same role+geo. The system already
|
||||||
|
// has every input (workers_500k, call_log, playbook_memory).
|
||||||
|
// The route binds them.
|
||||||
|
// No /i — the name has to be capitalized (English convention)
|
||||||
|
// and the event verbs are matched lowercase. The /i flag was
|
||||||
|
// letting "Marcus running" parse as "Marcus Running" (a last
|
||||||
|
// name) and then the event regex wouldn't find "running late"
|
||||||
|
// because "running" was already consumed by the name group.
|
||||||
|
const triageMatch = q.match(/^([A-Z][a-z]+(?:\s+[A-Z]\.?\s*)?(?:\s+[A-Z][a-z]+)?)\s+(running\s+late|late|no\s*show|no-show|sick|out\s+today|called\s+out|called\s+in|can'?t\s+make\s+it|won'?t\s+make\s+it)/);
|
||||||
|
if (triageMatch) {
|
||||||
|
const name = triageMatch[1].trim();
|
||||||
|
const event = triageMatch[2].toLowerCase().replace(/\s+/g, " ");
|
||||||
|
queries.push(`SQL: locate ${name}'s worker record`);
|
||||||
|
const profileR = await api("POST", "/query/sql", { sql: `SELECT name, role, city, state, zip, ROUND(CAST(reliability AS DOUBLE),2) rel, ROUND(CAST(availability AS DOUBLE),2) avail, ROUND(CAST(responsiveness AS DOUBLE),2) resp, archetype, skills, certifications FROM workers_500k WHERE name LIKE '%${name.replace(/'/g, "''")}%' ORDER BY CAST(reliability AS DOUBLE) DESC LIMIT 1` });
|
||||||
|
if (profileR.rows?.length) {
|
||||||
|
const w = profileR.rows[0];
|
||||||
|
// Pull attendance pattern from call_log if available — count
|
||||||
|
// recent calls + count of unanswered/late patterns. If the
|
||||||
|
// table doesn't exist or has nothing, we surface that
|
||||||
|
// honestly rather than fabricate.
|
||||||
|
queries.push(`SQL: ${w.name}'s recent contact pattern`);
|
||||||
|
const callR = await api("POST", "/query/sql", { sql: `SELECT COUNT(*) calls FROM call_log WHERE candidate_id IN (SELECT candidate_id FROM workers_500k WHERE name = '${w.name.replace(/'/g, "''")}')` }).catch(() => null);
|
||||||
|
const callCount = callR?.rows?.[0]?.calls ?? null;
|
||||||
|
|
||||||
|
// Backfills: same role + same geo, available now, ordered
|
||||||
|
// by responsiveness (a coordinator covering a no-show
|
||||||
|
// wants the candidate who actually answers their phone).
|
||||||
|
queries.push(`Backfill: ${w.role} in ${w.city}, ${w.state}, available, sorted by responsiveness`);
|
||||||
|
const backfillR = await api("POST", "/query/sql", { sql: `SELECT name, role, city, state, zip, ROUND(CAST(reliability AS DOUBLE),2) rel, ROUND(CAST(availability AS DOUBLE),2) avail, ROUND(CAST(responsiveness AS DOUBLE),2) resp, archetype, skills FROM workers_500k WHERE role = '${w.role.replace(/'/g, "''")}' AND city = '${(w.city||"").replace(/'/g, "''")}' AND state = '${(w.state||"").replace(/'/g, "''")}' AND name != '${w.name.replace(/'/g, "''")}' AND CAST(availability AS DOUBLE) > 0.6 ORDER BY CAST(responsiveness AS DOUBLE) DESC, CAST(reliability AS DOUBLE) DESC LIMIT 5` });
|
||||||
|
|
||||||
|
// Draft SMS the coordinator can send to the client. This
|
||||||
|
// is template-generated, not LLM — the coordinator must
|
||||||
|
// be able to send it instantly without re-reading. Names
|
||||||
|
// and roles are interpolated; the COORDINATOR sends.
|
||||||
|
const eventLabel = event.includes("late") ? "running late" : event.includes("show") ? "a no-show" : event.includes("sick") || event.includes("out") ? "out today" : "unable to make their shift";
|
||||||
|
const backfills = backfillR.rows || [];
|
||||||
|
const topBackfill = backfills[0]?.name;
|
||||||
|
const draftSms = topBackfill
|
||||||
|
? `Heads-up: ${w.name} (${w.role}) is ${eventLabel}. I'm dispatching ${topBackfill} from our local bench (${Math.round((backfills[0].rel||0)*100)}% reliability) to cover. Will confirm arrival within the hour.`
|
||||||
|
: `Heads-up: ${w.name} (${w.role}) is ${eventLabel}. I'm pulling our nearest available ${w.role} now and will confirm coverage shortly.`;
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
type: "triage",
|
||||||
|
summary: `${w.name} — ${eventLabel}. ${backfills.length} local backfill${backfills.length === 1 ? "" : "s"} ready, draft SMS ready to send.`,
|
||||||
|
worker: { name: w.name, role: w.role, city: w.city, state: w.state, zip: w.zip, rel: w.rel, avail: w.avail, resp: w.resp, archetype: w.archetype, skills: w.skills, certifications: w.certifications, recent_calls: callCount },
|
||||||
|
event,
|
||||||
|
backfills,
|
||||||
|
draft_sms: draftSms,
|
||||||
|
queries_run: queries,
|
||||||
|
duration_ms: Date.now() - start,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ok({ type: "triage_miss", summary: `Couldn't find a worker named "${name}" in the roster. Check the spelling or try last name only.`, queries_run: queries, duration_ms: Date.now() - start });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route 7: bare-name profile lookup. Coordinator types just a
|
||||||
|
// name (or "First Last") with no other intent — pull the
|
||||||
|
// profile, prior fills, and attendance pattern in one shot.
|
||||||
|
// Distinguished from smart_search by being SHORT (≤4 tokens),
|
||||||
|
// capitalized like a name, and not containing role/skill words.
|
||||||
|
const tokens = q.trim().split(/\s+/);
|
||||||
|
const looksLikeName = tokens.length >= 1 && tokens.length <= 4
|
||||||
|
&& tokens.every((t) => /^[A-Z][a-z'-]+\.?$/.test(t) || /^[A-Z]\.$/.test(t))
|
||||||
|
&& !/forklift|warehouse|electric|welder|assembl|maintain|production|operator|driver|tech|loader|packag|inventory|sanitation/i.test(q);
|
||||||
|
if (looksLikeName) {
|
||||||
|
// Names have middle initials in workers_500k ("Steven A. Allen"),
|
||||||
|
// so a single LIKE '%First Last%' won't match. Split on
|
||||||
|
// whitespace, AND each token — lets "Marcus Rivera" match
|
||||||
|
// "Marcus L. Rivera" without enumerating initials.
|
||||||
|
const nameLike = tokens
|
||||||
|
.map((t) => `name LIKE '%${t.replace(/'/g, "''").replace(/\./g, "")}%'`)
|
||||||
|
.join(" AND ");
|
||||||
|
queries.push(`SQL: lookup name="${q}" via per-token LIKE`);
|
||||||
|
const r = await api("POST", "/query/sql", { sql: `SELECT name, role, city, state, zip, ROUND(CAST(reliability AS DOUBLE),2) rel, ROUND(CAST(availability AS DOUBLE),2) avail, ROUND(CAST(responsiveness AS DOUBLE),2) resp, archetype, skills, certifications FROM workers_500k WHERE ${nameLike} ORDER BY CAST(reliability AS DOUBLE) DESC LIMIT 5` });
|
||||||
|
if (r.rows?.length) {
|
||||||
|
return ok({
|
||||||
|
type: "profile",
|
||||||
|
summary: r.rows.length === 1 ? `${r.rows[0].name} — ${r.rows[0].role}, ${r.rows[0].city}, ${r.rows[0].state}` : `${r.rows.length} workers match "${q}"`,
|
||||||
|
profiles: r.rows,
|
||||||
|
queries_run: queries,
|
||||||
|
duration_ms: Date.now() - start,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ok({ type: "profile_miss", summary: `No workers named "${q}" in the roster.`, queries_run: queries, duration_ms: Date.now() - start });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route 8: temporal — "what came in last night", "new resumes
|
||||||
|
// today", "last 24 hours". Surfaces recent ingest events from
|
||||||
|
// the catalog (created_at on dataset objects) and ranks them
|
||||||
|
// against open job_orders for "likely role match." Schema-
|
||||||
|
// agnostic: any dataset that landed recently shows up.
|
||||||
|
const temporalMatch = lower.match(/\b(last\s+night|today|this\s+morning|past\s+(\d+)\s+(?:hours?|days?)|last\s+(\d+)\s+(?:hours?|days?)|recent|new\s+(?:resumes?|candidates?|workers?|hires?|today)|came\s+in|arrived|just\s+(?:got|came))/i);
|
||||||
|
if (temporalMatch) {
|
||||||
|
// Decide window in hours
|
||||||
|
let windowHours = 24;
|
||||||
|
const pastN = lower.match(/\b(?:past|last)\s+(\d+)\s+(hours?|days?)/);
|
||||||
|
if (pastN) {
|
||||||
|
windowHours = parseInt(pastN[1], 10) * (pastN[2].startsWith("d") ? 24 : 1);
|
||||||
|
} else if (/last\s+night|this\s+morning|today/i.test(lower)) {
|
||||||
|
windowHours = 24;
|
||||||
|
} else if (/recent/i.test(lower)) {
|
||||||
|
windowHours = 72;
|
||||||
|
}
|
||||||
|
queries.push(`Catalog: datasets with created_at within last ${windowHours}h`);
|
||||||
|
const ds = await api("GET", "/catalog/datasets") as any[];
|
||||||
|
const cutoff = Date.now() - windowHours * 3600 * 1000;
|
||||||
|
const recent = (Array.isArray(ds) ? ds : [])
|
||||||
|
.map((d: any) => ({
|
||||||
|
name: d.name,
|
||||||
|
row_count: d.row_count || 0,
|
||||||
|
bytes: (d.objects?.[0]?.size_bytes) || 0,
|
||||||
|
updated_at: d.updated_at,
|
||||||
|
ts: d.updated_at ? Date.parse(d.updated_at) : 0,
|
||||||
|
}))
|
||||||
|
.filter((d) => d.ts >= cutoff && d.row_count > 0)
|
||||||
|
.sort((a, b) => b.ts - a.ts);
|
||||||
|
|
||||||
|
// For each recent dataset, sample its first row's role-shape
|
||||||
|
// text so the coordinator sees what's in it without reading
|
||||||
|
// schemas. If it's a workers/resumes dataset, group by role.
|
||||||
|
const samples: any[] = [];
|
||||||
|
for (const d of recent.slice(0, 8)) {
|
||||||
|
const sample = await api("POST", "/query/sql", { sql: `SELECT * FROM "${d.name.replace(/"/g, '""')}" LIMIT 1` }).catch(() => null);
|
||||||
|
const cols = sample?.columns?.map((c: any) => c.name) || [];
|
||||||
|
const looksLikeWorkers = cols.includes("role") && (cols.includes("name") || cols.includes("candidate_id"));
|
||||||
|
let roleBreakdown: any[] = [];
|
||||||
|
if (looksLikeWorkers) {
|
||||||
|
const byRole = await api("POST", "/query/sql", { sql: `SELECT role, COUNT(*) cnt FROM "${d.name.replace(/"/g, '""')}" GROUP BY role ORDER BY cnt DESC LIMIT 5` }).catch(() => null);
|
||||||
|
roleBreakdown = byRole?.rows || [];
|
||||||
|
}
|
||||||
|
samples.push({
|
||||||
|
name: d.name,
|
||||||
|
row_count: d.row_count,
|
||||||
|
updated_at: d.updated_at,
|
||||||
|
hours_ago: Math.round((Date.now() - d.ts) / 3600000),
|
||||||
|
looks_like_workers: looksLikeWorkers,
|
||||||
|
role_breakdown: roleBreakdown,
|
||||||
|
preview: sample?.rows?.[0] || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok({
|
||||||
|
type: "ingest_log",
|
||||||
|
summary: recent.length
|
||||||
|
? `${recent.length} dataset${recent.length === 1 ? "" : "s"} landed in the last ${windowHours}h. ${samples.filter((s) => s.looks_like_workers).reduce((sum, s) => sum + s.row_count, 0)} new worker rows across them.`
|
||||||
|
: `Nothing new in the catalog in the last ${windowHours}h. (Dataset timestamps are based on catalog updated_at; if data was loaded directly to disk without going through /ingest/file, it won't show here.)`,
|
||||||
|
window_hours: windowHours,
|
||||||
|
datasets: samples,
|
||||||
|
queries_run: queries,
|
||||||
|
duration_ms: Date.now() - start,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Default: smart search — extract role, location, availability from natural language
|
// Default: smart search — extract role, location, availability from natural language
|
||||||
{
|
{
|
||||||
const filters: string[] = ["CAST(reliability AS DOUBLE) >= 0.5"];
|
const filters: string[] = ["CAST(reliability AS DOUBLE) >= 0.5"];
|
||||||
const understood: string[] = [];
|
const understood: string[] = [];
|
||||||
|
|
||||||
// Extract role keywords
|
// Structured input from the search-form dropdowns. When set,
|
||||||
|
// these win over NL parsing — typing "forklift in IL" used to
|
||||||
|
// misparse the preposition "in" as state IN (Indiana). Trust
|
||||||
|
// explicit user selection over regex archaeology.
|
||||||
|
const explicitState = String(b.state || "").trim().toUpperCase();
|
||||||
|
const explicitRole = String(b.role || "").trim();
|
||||||
|
|
||||||
|
// (G) Per-staffer context. When the UI sends a staffer_id,
|
||||||
|
// playbook queries scope to that staffer's territory — their
|
||||||
|
// recent fills, their geo's recurring patterns. The corpus is
|
||||||
|
// the same for everyone; the relevance gradient is unique to
|
||||||
|
// each staffer because each pulls a different shape from it.
|
||||||
|
const staffer = lookupStaffer(String(b.staffer_id || "").trim());
|
||||||
|
// If the staffer has a territory and the user hasn't already
|
||||||
|
// pinned a state/city via dropdown or NL, default the search
|
||||||
|
// to their territory. They can override by typing a different
|
||||||
|
// city or selecting a different state.
|
||||||
|
if (staffer && !explicitState) {
|
||||||
|
filters.push(`state = '${staffer.territory.state}'`);
|
||||||
|
understood.push(`as ${staffer.name}: ${staffer.territory.state}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (B) Headcount parser — coordinator says "8 production
|
||||||
|
// workers", "I need 12 forklift operators", "5 welders by
|
||||||
|
// Friday". Match a leading or embedded count followed by
|
||||||
|
// a worker-shape noun. Bound at 1..200 — anything outside is
|
||||||
|
// probably not a headcount (zip codes, dates, addresses).
|
||||||
|
let topK = 10;
|
||||||
|
// Allow zero-to-two role words between the number and the
|
||||||
|
// worker-noun: "8 workers" / "8 production workers" /
|
||||||
|
// "8 forklift operators" all match. The role word is
|
||||||
|
// optional so we don't lose the bare-number form.
|
||||||
|
const countMatch = q.match(/\b(\d{1,3})\s+(?:\w+\s+){0,2}(?:workers?|operators?|drivers?|techs?|technicians?|welders?|electricians?|assemblers?|handlers?|loaders?|packagers?|associates?|leads?|people|hires?|staff)\b/i);
|
||||||
|
if (countMatch) {
|
||||||
|
const n = parseInt(countMatch[1], 10);
|
||||||
|
if (n >= 1 && n <= 200) {
|
||||||
|
topK = n;
|
||||||
|
understood.push(`headcount: ${n}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (A) Zip code → city/state lookup. A coordinator types a zip
|
||||||
|
// because that's what the contract says. The previous parser
|
||||||
|
// saw "60607" and treated it as a stray number; results came
|
||||||
|
// back from any state. Map known metro zip prefixes here so
|
||||||
|
// the geographic constraint actually fires.
|
||||||
|
//
|
||||||
|
// Each entry: zip-prefix → { city, state }. Prefix-match
|
||||||
|
// covers a metro without enumerating every zip — e.g. "606"
|
||||||
|
// catches Chicago zips 60600-60699.
|
||||||
|
const zipPrefixMap: Array<[string, { city: string, state: string }]> = [
|
||||||
|
// Chicago + near-suburb
|
||||||
|
["606", { city: "Chicago", state: "IL" }],
|
||||||
|
["607", { city: "Chicago", state: "IL" }],
|
||||||
|
["608", { city: "Chicago", state: "IL" }],
|
||||||
|
// Indianapolis
|
||||||
|
["462", { city: "Indianapolis", state: "IN" }],
|
||||||
|
["461", { city: "Indianapolis", state: "IN" }],
|
||||||
|
// Fort Wayne
|
||||||
|
["468", { city: "Fort Wayne", state: "IN" }],
|
||||||
|
// Columbus OH
|
||||||
|
["432", { city: "Columbus", state: "OH" }],
|
||||||
|
["431", { city: "Columbus", state: "OH" }],
|
||||||
|
// Cleveland
|
||||||
|
["441", { city: "Cleveland", state: "OH" }],
|
||||||
|
// Cincinnati
|
||||||
|
["452", { city: "Cincinnati", state: "OH" }],
|
||||||
|
["451", { city: "Cincinnati", state: "OH" }],
|
||||||
|
// Dayton
|
||||||
|
["454", { city: "Dayton", state: "OH" }],
|
||||||
|
// Milwaukee
|
||||||
|
["532", { city: "Milwaukee", state: "WI" }],
|
||||||
|
["531", { city: "Milwaukee", state: "WI" }],
|
||||||
|
// Madison
|
||||||
|
["537", { city: "Madison", state: "WI" }],
|
||||||
|
// Detroit
|
||||||
|
["482", { city: "Detroit", state: "MI" }],
|
||||||
|
["481", { city: "Detroit", state: "MI" }],
|
||||||
|
// Grand Rapids
|
||||||
|
["495", { city: "Grand Rapids", state: "MI" }],
|
||||||
|
["493", { city: "Grand Rapids", state: "MI" }],
|
||||||
|
// Minneapolis / St. Paul
|
||||||
|
["554", { city: "Minneapolis", state: "MN" }],
|
||||||
|
["551", { city: "Minneapolis", state: "MN" }],
|
||||||
|
// Des Moines
|
||||||
|
["503", { city: "Des Moines", state: "IA" }],
|
||||||
|
// Kansas City MO
|
||||||
|
["641", { city: "Kansas City", state: "MO" }],
|
||||||
|
// St. Louis
|
||||||
|
["631", { city: "St. Louis", state: "MO" }],
|
||||||
|
// Nashville
|
||||||
|
["372", { city: "Nashville", state: "TN" }],
|
||||||
|
// Memphis
|
||||||
|
["381", { city: "Memphis", state: "TN" }],
|
||||||
|
// Knoxville
|
||||||
|
["379", { city: "Knoxville", state: "TN" }],
|
||||||
|
// Louisville
|
||||||
|
["402", { city: "Louisville", state: "KY" }],
|
||||||
|
// Lexington
|
||||||
|
["405", { city: "Lexington", state: "KY" }],
|
||||||
|
];
|
||||||
|
const zipMatch = q.match(/\b(\d{5})\b/);
|
||||||
|
let zipCity: { city: string, state: string } | null = null;
|
||||||
|
if (zipMatch) {
|
||||||
|
const z = zipMatch[1];
|
||||||
|
const hit = zipPrefixMap.find(([prefix]) => z.startsWith(prefix));
|
||||||
|
if (hit) {
|
||||||
|
zipCity = hit[1];
|
||||||
|
understood.push(`zip ${z} → ${hit[1].city}, ${hit[1].state}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract role keywords (skip if dropdown picked one)
|
||||||
const roleKeywords: Record<string, string> = {
|
const roleKeywords: Record<string, string> = {
|
||||||
"warehouse": "warehouse", "forklift": "forklift", "welder": "weld", "assembler": "assembl",
|
"warehouse": "warehouse", "forklift": "forklift", "welder": "weld", "assembler": "assembl",
|
||||||
"loader": "loader", "machine operator": "machine operator", "shipping": "shipping",
|
"loader": "loader", "machine operator": "machine operator", "shipping": "shipping",
|
||||||
@ -1707,37 +2333,64 @@ async function main() {
|
|||||||
"line lead": "line lead", "electrician": "electric", "packaging": "packaging",
|
"line lead": "line lead", "electrician": "electric", "packaging": "packaging",
|
||||||
"tool and die": "tool", "logistics": "logistics", "safety": "safety", "cnc": "cnc",
|
"tool and die": "tool", "logistics": "logistics", "safety": "safety", "cnc": "cnc",
|
||||||
};
|
};
|
||||||
for (const [kw, sqlPart] of Object.entries(roleKeywords)) {
|
if (explicitRole) {
|
||||||
if (lower.includes(kw)) { filters.push(`LOWER(role) LIKE '%${sqlPart}%'`); understood.push(`role: ${kw}`); break; }
|
filters.push(`LOWER(role) LIKE '%${explicitRole.toLowerCase().replace(/'/g, "''")}%'`);
|
||||||
}
|
understood.push(`role: ${explicitRole}`);
|
||||||
|
} else {
|
||||||
// Extract city
|
for (const [kw, sqlPart] of Object.entries(roleKeywords)) {
|
||||||
const cities = ["chicago","springfield","rockford","peoria","joliet","indianapolis","fort wayne",
|
if (lower.includes(kw)) { filters.push(`LOWER(role) LIKE '%${sqlPart}%'`); understood.push(`role: ${kw}`); break; }
|
||||||
"evansville","south bend","columbus","cleveland","cincinnati","dayton","akron","toledo",
|
|
||||||
"st. louis","st louis","kansas city","nashville","memphis","knoxville","louisville","lexington",
|
|
||||||
"milwaukee","madison","detroit","grand rapids","lansing","des moines","minneapolis","terre haute",
|
|
||||||
"bloomington","decatur","mattoon","galesburg","danville","champaign"];
|
|
||||||
for (const city of cities) {
|
|
||||||
if (lower.includes(city)) {
|
|
||||||
const sqlCity = city.split(' ').map(w => w[0].toUpperCase() + w.slice(1)).join(' ');
|
|
||||||
filters.push(`city = '${sqlCity}'`);
|
|
||||||
understood.push(`city: ${sqlCity}`);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract state
|
// Extract city
|
||||||
|
// Zip code wins over city-name parsing — it's more specific
|
||||||
|
// and the coordinator typed a number, not a casual mention.
|
||||||
|
if (zipCity) {
|
||||||
|
filters.push(`city = '${zipCity.city}'`);
|
||||||
|
understood.push(`city: ${zipCity.city}`);
|
||||||
|
} else {
|
||||||
|
const cities = ["chicago","springfield","rockford","peoria","joliet","indianapolis","fort wayne",
|
||||||
|
"evansville","south bend","columbus","cleveland","cincinnati","dayton","akron","toledo",
|
||||||
|
"st. louis","st louis","kansas city","nashville","memphis","knoxville","louisville","lexington",
|
||||||
|
"milwaukee","madison","detroit","grand rapids","lansing","des moines","minneapolis","terre haute",
|
||||||
|
"bloomington","decatur","mattoon","galesburg","danville","champaign"];
|
||||||
|
for (const city of cities) {
|
||||||
|
if (lower.includes(city)) {
|
||||||
|
const sqlCity = city.split(' ').map(w => w[0].toUpperCase() + w.slice(1)).join(' ');
|
||||||
|
filters.push(`city = '${sqlCity}'`);
|
||||||
|
understood.push(`city: ${sqlCity}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract state — dropdown wins; otherwise NL parse, but
|
||||||
|
// require either an explicit "in/from <STATE>" preposition
|
||||||
|
// OR an UPPERCASE 2-letter code, never a bare lowercase
|
||||||
|
// 2-letter token. Old regex matched "in" (preposition) as
|
||||||
|
// state IN (Indiana) because the /i flag made the standalone
|
||||||
|
// pattern case-insensitive — "forklift in IL" always returned
|
||||||
|
// Indiana workers.
|
||||||
const stateNames: Record<string, string> = {
|
const stateNames: Record<string, string> = {
|
||||||
"illinois":"IL","indiana":"IN","ohio":"OH","missouri":"MO","tennessee":"TN",
|
"illinois":"IL","indiana":"IN","ohio":"OH","missouri":"MO","tennessee":"TN",
|
||||||
"kentucky":"KY","wisconsin":"WI","michigan":"MI","iowa":"IA","minnesota":"MN"
|
"kentucky":"KY","wisconsin":"WI","michigan":"MI","iowa":"IA","minnesota":"MN"
|
||||||
};
|
};
|
||||||
const stateMatch = lower.match(/\b(IL|IN|OH|MO|TN|KY|WI|MI|IA|MN)\b/i);
|
if (explicitState) {
|
||||||
if (stateMatch && !understood.some(u => u.startsWith('city'))) {
|
if (!understood.some(u => u.startsWith('city'))) {
|
||||||
filters.push(`state = '${stateMatch[1].toUpperCase()}'`);
|
filters.push(`state = '${explicitState.replace(/'/g, "''")}'`);
|
||||||
understood.push(`state: ${stateMatch[1].toUpperCase()}`);
|
understood.push(`state: ${explicitState}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [name, abbr] of Object.entries(stateNames)) {
|
const prepMatch = q.match(/\b(?:in|from)\s+(IL|IN|OH|MO|TN|KY|WI|MI|IA|MN)\b/i);
|
||||||
if (lower.includes(name)) { filters.push(`state = '${abbr}'`); understood.push(`state: ${abbr}`); break; }
|
const upperMatch = q.match(/\b(IL|IN|OH|MO|TN|KY|WI|MI|IA|MN)\b/); // no /i — must be uppercase
|
||||||
|
const stateMatch = prepMatch || upperMatch;
|
||||||
|
if (stateMatch && !understood.some(u => u.startsWith('city'))) {
|
||||||
|
filters.push(`state = '${stateMatch[1].toUpperCase()}'`);
|
||||||
|
understood.push(`state: ${stateMatch[1].toUpperCase()}`);
|
||||||
|
} else {
|
||||||
|
for (const [name, abbr] of Object.entries(stateNames)) {
|
||||||
|
if (lower.includes(name)) { filters.push(`state = '${abbr}'`); understood.push(`state: ${abbr}`); break; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1758,20 +2411,31 @@ async function main() {
|
|||||||
queries.push("SQL filter: " + filterStr);
|
queries.push("SQL filter: " + filterStr);
|
||||||
queries.push("Vector: semantic search for best skill match");
|
queries.push("Vector: semantic search for best skill match");
|
||||||
|
|
||||||
// Also run a direct SQL query to get exact counts and zip codes
|
// Also run a direct SQL query to get exact counts and zip codes.
|
||||||
|
// LIMIT honors the parsed headcount (capped at 25 to keep the
|
||||||
|
// grid renderable; the staffer can ask for more).
|
||||||
const sqlFields = "name, role, city, state, zip, ROUND(CAST(reliability AS DOUBLE),2) rel, ROUND(CAST(availability AS DOUBLE),2) avail, skills, certifications, archetype";
|
const sqlFields = "name, role, city, state, zip, ROUND(CAST(reliability AS DOUBLE),2) rel, ROUND(CAST(availability AS DOUBLE),2) avail, skills, certifications, archetype";
|
||||||
const directSql = `SELECT ${sqlFields} FROM workers_500k WHERE ${filterStr} ORDER BY CAST(availability AS DOUBLE) DESC, CAST(reliability AS DOUBLE) DESC LIMIT 10`;
|
const sqlLimit = Math.min(Math.max(topK, 5), 25);
|
||||||
|
const directSql = `SELECT ${sqlFields} FROM workers_500k WHERE ${filterStr} ORDER BY CAST(availability AS DOUBLE) DESC, CAST(reliability AS DOUBLE) DESC LIMIT ${sqlLimit}`;
|
||||||
|
|
||||||
// Derive role+geo for the pattern query so the meta-index
|
// Derive role+geo for the pattern query so the meta-index
|
||||||
// surface lines up with what the user actually asked for.
|
// surface lines up with what the user actually asked for.
|
||||||
|
// (G) When a staffer is acting, default the geo to their
|
||||||
|
// primary territory — their playbook view is shaped by
|
||||||
|
// where they actually fill, not the global Chicago/IL prior.
|
||||||
const roleForPatterns = understood.find(u => u.startsWith('role:'))?.split(': ')[1] || q;
|
const roleForPatterns = understood.find(u => u.startsWith('role:'))?.split(': ')[1] || q;
|
||||||
const cityForPatterns = understood.find(u => u.startsWith('city:'))?.split(': ')[1] || 'Chicago';
|
const cityForPatterns = understood.find(u => u.startsWith('city:'))?.split(': ')[1]
|
||||||
const stateForPatterns = understood.find(u => u.startsWith('state:'))?.split(': ')[1] || 'IL';
|
|| staffer?.territory.cities[0] || 'Chicago';
|
||||||
|
const stateForPatterns = understood.find(u => u.startsWith('state:'))?.split(': ')[1]
|
||||||
|
|| staffer?.territory.state || 'IL';
|
||||||
|
|
||||||
const [searchR, directR, patternR] = await Promise.all([
|
const [searchR, directR, patternR] = await Promise.all([
|
||||||
api("POST", "/vectors/hybrid", {
|
api("POST", "/vectors/hybrid", {
|
||||||
question: q, index_name: "workers_500k_v1", sql_filter: filterStr,
|
question: q, index_name: "workers_500k_v1", sql_filter: filterStr,
|
||||||
filter_dataset: "ethereal_workers", id_column: "worker_id", top_k: 8, generate: false,
|
filter_dataset: "ethereal_workers", id_column: "worker_id",
|
||||||
|
// Honor the parsed headcount (capped at 25 to keep the
|
||||||
|
// vector rerank from re-scoring more rows than render).
|
||||||
|
top_k: Math.min(Math.max(topK, 5), 25), generate: false,
|
||||||
// k=200 to catch compounding — direct measurement shows
|
// k=200 to catch compounding — direct measurement shows
|
||||||
// boost reliably fires only when ~all memory is scanned
|
// boost reliably fires only when ~all memory is scanned
|
||||||
// due to the narrow 0.55-0.67 cosine band in the 768d
|
// due to the narrow 0.55-0.67 cosine band in the 768d
|
||||||
@ -1797,6 +2461,7 @@ async function main() {
|
|||||||
return ok({
|
return ok({
|
||||||
type: "smart_search",
|
type: "smart_search",
|
||||||
summary: `Found ${searchR.sql_matches || 0} workers matching your criteria${understood.length ? ' (' + understood.join(', ') + ')' : ''}`,
|
summary: `Found ${searchR.sql_matches || 0} workers matching your criteria${understood.length ? ' (' + understood.join(', ') + ')' : ''}`,
|
||||||
|
staffer: staffer ? { id: staffer.id, name: staffer.name, display: staffer.display, territory: staffer.territory } : null,
|
||||||
understood,
|
understood,
|
||||||
sql_results: sqlWorkers,
|
sql_results: sqlWorkers,
|
||||||
vector_results: vectorWorkers,
|
vector_results: vectorWorkers,
|
||||||
|
|||||||
599
mcp-server/profiler.html
Normal file
599
mcp-server/profiler.html
Normal file
@ -0,0 +1,599 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head>
|
||||||
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Profiler Index · Staffing Co-Pilot</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html,body{overflow-x:hidden}
|
||||||
|
body{font-family:'Inter',-apple-system,system-ui,sans-serif;background:#090c10;color:#b0b8c4;font-size:14px;line-height:1.6}
|
||||||
|
.bar{background:#0d1117;padding:0 24px;height:56px;border-bottom:1px solid #171d27;display:flex;justify-content:space-between;align-items:center}
|
||||||
|
.bar h1{font-size:14px;font-weight:600;color:#e6edf3}
|
||||||
|
.bar nav a{color:#545d68;text-decoration:none;font-size:12px;padding:6px 14px;border-radius:6px;margin-left:4px}
|
||||||
|
.bar nav a:hover{color:#e6edf3;background:#161b22}
|
||||||
|
.content{max-width:1200px;margin:0 auto;padding:24px 20px 40px}
|
||||||
|
.controls{background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:16px;margin-bottom:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
||||||
|
.controls input,.controls select{padding:9px 12px;background:#161b22;border:1px solid #21262d;border-radius:6px;color:#e6edf3;font-size:13px;outline:none}
|
||||||
|
.controls input:focus,.controls select:focus{border-color:#388bfd}
|
||||||
|
.controls input.s{flex:1;min-width:240px}
|
||||||
|
.controls .meta{font-size:11px;color:#8b949e;margin-left:auto}
|
||||||
|
.summary{background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:14px 16px;margin-bottom:14px;font-size:12px;color:#8b949e}
|
||||||
|
.summary b{color:#e6edf3;font-weight:600}
|
||||||
|
table{width:100%;border-collapse:collapse;background:#0d1117;border:1px solid #171d27;border-radius:10px;overflow:hidden}
|
||||||
|
th{font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;font-weight:600;text-align:left;padding:12px;background:#0a0d12;border-bottom:1px solid #171d27;cursor:pointer;user-select:none}
|
||||||
|
th:hover{color:#e6edf3}
|
||||||
|
th .arrow{font-size:9px;margin-left:4px;color:#388bfd}
|
||||||
|
td{padding:11px 12px;border-bottom:1px solid #1f2631;font-size:13px}
|
||||||
|
tr:last-child td{border-bottom:none}
|
||||||
|
tr:hover td{background:#0a0d12}
|
||||||
|
td.name a{color:#58a6ff;text-decoration:none;font-weight:600}
|
||||||
|
td.name a:hover{text-decoration:underline}
|
||||||
|
td.right{text-align:right;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}
|
||||||
|
td.role{font-size:10px;color:#8b949e}
|
||||||
|
td.role .pill{display:inline-block;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#161b22;border:1px solid #21262d;color:#8b949e;margin-right:4px;text-transform:uppercase;letter-spacing:0.5px}
|
||||||
|
.tickers{display:flex;gap:4px;flex-wrap:wrap;margin-top:3px}
|
||||||
|
.ticker-pill{display:inline-block;padding:1px 7px;border-radius:5px;font-size:10px;font-weight:700;font-family:ui-monospace,SFMono-Regular,monospace;letter-spacing:0.3px;cursor:help}
|
||||||
|
.ticker-pill.direct{background:#0d2818;border:1px solid #2ea04388;color:#3fb950}
|
||||||
|
.ticker-pill.parent{background:#1a1410;border:1px solid #d2992288;color:#d29922}
|
||||||
|
.ticker-pill.associated{background:#0d1830;border:1px solid #58a6ff66;color:#58a6ff}
|
||||||
|
.ticker-pill.exact{background:#0d2818;border:1px solid #2ea043;color:#3fb950}
|
||||||
|
|
||||||
|
/* Hero — the thesis panel that frames the data corpus's value. */
|
||||||
|
.thesis{background:linear-gradient(135deg,#0d2818 0%,#0d1830 50%,#1a1410 100%);border:1px solid #2ea04344;border-radius:12px;padding:18px 22px;margin-bottom:14px;position:relative;overflow:hidden}
|
||||||
|
.thesis::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#3fb950 0%,#58a6ff 50%,#d29922 100%)}
|
||||||
|
.thesis h2{font-size:18px;color:#e6edf3;font-weight:700;letter-spacing:-0.4px;margin-bottom:6px}
|
||||||
|
.thesis .sub{font-size:12px;color:#8b949e;line-height:1.6;margin-bottom:14px;max-width:880px}
|
||||||
|
.thesis .sub b{color:#3fb950;font-weight:600}
|
||||||
|
.bai-row{display:flex;gap:24px;align-items:baseline;flex-wrap:wrap;margin-bottom:14px}
|
||||||
|
.bai-block{display:flex;flex-direction:column;gap:2px}
|
||||||
|
.bai-label{font-size:9px;color:#545d68;text-transform:uppercase;letter-spacing:1.4px;font-weight:700}
|
||||||
|
.bai-value{font-size:26px;font-weight:700;color:#e6edf3;font-family:ui-monospace,monospace;letter-spacing:-0.5px;font-variant-numeric:tabular-nums}
|
||||||
|
.bai-value.up{color:#3fb950}
|
||||||
|
.bai-value.down{color:#f85149}
|
||||||
|
.bai-sub{font-size:10px;color:#8b949e;margin-top:1px}
|
||||||
|
.markets-strip{display:flex;gap:6px;flex-wrap:wrap;font-size:10px}
|
||||||
|
.market-pill{padding:3px 9px;border-radius:9px;font-weight:600;border:1px solid;letter-spacing:0.4px}
|
||||||
|
.market-pill.live{background:#0d2818;border-color:#3fb950;color:#3fb950}
|
||||||
|
.market-pill.next{background:#0d1830;border-color:#58a6ff;color:#58a6ff}
|
||||||
|
.market-pill.queue{background:#161b22;border-color:#21262d;color:#545d68}
|
||||||
|
.market-pill.queue::before{content:'· '}
|
||||||
|
|
||||||
|
/* Map panel below basket — populates when a ticker is selected. */
|
||||||
|
.signal-map-wrap{display:none;background:#0d1117;border:1px solid #171d27;border-radius:10px;padding:14px;margin-bottom:14px}
|
||||||
|
.signal-map-wrap.active{display:block}
|
||||||
|
.signal-map-header{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px}
|
||||||
|
.signal-map-title{font-size:11px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;font-weight:600}
|
||||||
|
.signal-map-title b{color:#58a6ff;font-family:ui-monospace,monospace}
|
||||||
|
.signal-map-meta{font-size:11px;color:#8b949e}
|
||||||
|
.signal-map{height:340px;border-radius:8px;border:1px solid #1f2631;overflow:hidden}
|
||||||
|
.signal-map .leaflet-container{background:#0a0d12}
|
||||||
|
|
||||||
|
/* Scrolling ticker basket — top strip showing every public issuer
|
||||||
|
the profiler index has surfaced, with live price + day-change. */
|
||||||
|
.basket-wrap{background:#0a0d12;border:1px solid #171d27;border-radius:10px;margin-bottom:14px;overflow:hidden;position:relative}
|
||||||
|
.basket-label{padding:10px 16px 4px;font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.3px;font-weight:600;display:flex;justify-content:space-between;align-items:baseline}
|
||||||
|
.basket-label .meta{font-weight:400;color:#3d444d;font-size:10px;text-transform:none;letter-spacing:0}
|
||||||
|
.basket-track{display:flex;gap:0;overflow-x:auto;scroll-behavior:smooth;padding:6px 8px 12px;scrollbar-width:thin;scrollbar-color:#21262d transparent}
|
||||||
|
.basket-track::-webkit-scrollbar{height:6px}
|
||||||
|
.basket-track::-webkit-scrollbar-thumb{background:#21262d;border-radius:3px}
|
||||||
|
.basket-track::-webkit-scrollbar-thumb:hover{background:#388bfd}
|
||||||
|
.bk-card{flex:0 0 auto;min-width:140px;background:#0d1117;border:1px solid #21262d;border-radius:8px;padding:10px 12px;margin:0 4px;cursor:pointer;transition:all 0.12s;position:relative}
|
||||||
|
.bk-card:hover{border-color:#58a6ff;background:#0d1a30;transform:translateY(-1px)}
|
||||||
|
.bk-card.selected{border-color:#58a6ff;background:#0d1a30;box-shadow:0 0 0 1px #58a6ff;}
|
||||||
|
.bk-card .tk{font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;font-weight:700;color:#e6edf3;letter-spacing:0.3px}
|
||||||
|
.bk-card .px{font-family:ui-monospace,SFMono-Regular,monospace;font-size:14px;font-weight:600;color:#e6edf3;margin-top:3px;font-variant-numeric:tabular-nums}
|
||||||
|
.bk-card .ch{font-family:ui-monospace,monospace;font-size:11px;margin-top:1px;font-variant-numeric:tabular-nums}
|
||||||
|
.bk-card .ch.up{color:#3fb950}
|
||||||
|
.bk-card .ch.down{color:#f85149}
|
||||||
|
.bk-card .ch.flat{color:#545d68}
|
||||||
|
.bk-card .meta{font-size:9px;color:#545d68;margin-top:5px;text-transform:uppercase;letter-spacing:0.6px}
|
||||||
|
.bk-card .kind-bar{position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:8px 0 0 8px}
|
||||||
|
.bk-card .kind-bar.exact,.bk-card .kind-bar.direct{background:#3fb950}
|
||||||
|
.bk-card .kind-bar.parent{background:#d29922}
|
||||||
|
.bk-card .kind-bar.associated{background:#58a6ff}
|
||||||
|
.bk-card .kind-bar.mixed{background:linear-gradient(180deg,#3fb950 0%,#58a6ff 100%)}
|
||||||
|
.bk-card.no-quote .px{color:#545d68}
|
||||||
|
.basket-empty{padding:18px;font-size:11px;color:#545d68;font-style:italic;text-align:center}
|
||||||
|
.basket-clear{margin-left:8px;font-size:10px;color:#58a6ff;cursor:pointer;border:none;background:none;text-decoration:underline}
|
||||||
|
.cost-band-1{color:#3fb950}
|
||||||
|
.cost-band-2{color:#d29922}
|
||||||
|
.cost-band-3{color:#f85149}
|
||||||
|
.loading{text-align:center;padding:60px;font-size:13px;color:#3d444d}
|
||||||
|
.empty{text-align:center;padding:40px;font-size:12px;color:#545d68;font-style:italic}
|
||||||
|
.foot{margin-top:14px;font-size:10px;color:#484f58;line-height:1.6}
|
||||||
|
@media(max-width:640px){.bar{padding:0 14px}.content{padding:14px}th,td{padding:8px 6px;font-size:11px}}
|
||||||
|
</style>
|
||||||
|
</head><body>
|
||||||
|
<div class="bar">
|
||||||
|
<h1>Staffing Co-Pilot · Profiler Index</h1>
|
||||||
|
<nav>
|
||||||
|
<a href="" id="back-dashboard">← Dashboard</a>
|
||||||
|
<a href="" id="back-console">Console</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<!-- Hero thesis — frames what this data corpus actually is. The
|
||||||
|
profiler index isn't just a contractor directory; it's a
|
||||||
|
construction-activity signal that surfaces public issuers months
|
||||||
|
before quarterly earnings does. Each metric here is computed
|
||||||
|
from the live data, not pre-baked. -->
|
||||||
|
<div class="thesis" id="thesis">
|
||||||
|
<h2>Chicago Construction Activity Signal Engine</h2>
|
||||||
|
<div class="sub">
|
||||||
|
Every contractor name in this corpus is also a forward indicator on the public equities they touch. Permits filed today predict construction starts ~45 days out, staffing windows ~2 weeks before that, and revenue recognition months later. The associated-ticker network surfaces this signal <b>before</b> it lands in any 10-Q.
|
||||||
|
</div>
|
||||||
|
<div class="bai-row">
|
||||||
|
<div class="bai-block">
|
||||||
|
<span class="bai-label">Building Activity Index — today</span>
|
||||||
|
<span class="bai-value" id="bai-value">—</span>
|
||||||
|
<span class="bai-sub" id="bai-sub">awaiting basket prices</span>
|
||||||
|
</div>
|
||||||
|
<div class="bai-block">
|
||||||
|
<span class="bai-label">Indexed build value</span>
|
||||||
|
<span class="bai-value" id="bav-value">—</span>
|
||||||
|
<span class="bai-sub" id="bav-sub">across surfaced issuers</span>
|
||||||
|
</div>
|
||||||
|
<div class="bai-block">
|
||||||
|
<span class="bai-label">Network depth</span>
|
||||||
|
<span class="bai-value" id="net-value">—</span>
|
||||||
|
<span class="bai-sub" id="net-sub">issuers · attributions</span>
|
||||||
|
</div>
|
||||||
|
<div class="bai-block" style="flex:1;min-width:240px">
|
||||||
|
<span class="bai-label">Market replication roadmap</span>
|
||||||
|
<div class="markets-strip" style="margin-top:4px">
|
||||||
|
<span class="market-pill live">Chicago — live</span>
|
||||||
|
<span class="market-pill next">NYC DOB — adapter ready</span>
|
||||||
|
<span class="market-pill queue">LA County · Houston BCD · Boston ISD · DC DCRA</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="basket-wrap" id="basket-wrap" style="display:none">
|
||||||
|
<div class="basket-label">
|
||||||
|
<span><span id="bk-count">0</span> public issuers in this view <span class="meta" id="bk-meta"></span></span>
|
||||||
|
<button class="basket-clear" id="bk-clear" style="display:none" type="button">clear filter</button>
|
||||||
|
</div>
|
||||||
|
<div class="basket-track" id="basket"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Per-ticker permit map — shows where the selected issuer's
|
||||||
|
attributed contractor activity is actually happening. Same
|
||||||
|
leaflet pattern as the contractor profile, scoped to one ticker. -->
|
||||||
|
<div class="signal-map-wrap" id="signal-map-wrap">
|
||||||
|
<div class="signal-map-header">
|
||||||
|
<span class="signal-map-title">Where <b id="signal-map-ticker">—</b> activity is happening</span>
|
||||||
|
<span class="signal-map-meta" id="signal-map-meta">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="signal-map" id="signal-map"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<input class="s" id="q" type="text" placeholder="Filter by contractor name (e.g., Target, Turner)" autocomplete="off">
|
||||||
|
<select id="since">
|
||||||
|
<option value="2025-06-01">Since June 2025</option>
|
||||||
|
<option value="2024-01-01">Since 2024</option>
|
||||||
|
<option value="2020-01-01">Since 2020 (deeper history)</option>
|
||||||
|
</select>
|
||||||
|
<select id="min-cost">
|
||||||
|
<option value="500000">$500K+</option>
|
||||||
|
<option value="250000" selected>$250K+</option>
|
||||||
|
<option value="100000">$100K+</option>
|
||||||
|
<option value="50000">$50K+</option>
|
||||||
|
</select>
|
||||||
|
<span class="meta" id="meta">Loading…</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary" id="summary" style="display:none"></div>
|
||||||
|
<div id="result"><div class="loading">Loading the directory from Chicago Socrata…</div></div>
|
||||||
|
<div class="foot">Aggregations sourced live from data.cityofchicago.org (Building Permits dataset ydr8-5enu). Contractor names appear when listed as contact_1 or contact_2 on a permit. Click any name to open the full profile — heat map, project index, history, 12 awaiting public-data sources.</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var P=location.pathname.indexOf('/lakehouse')>=0?'/lakehouse':'';
|
||||||
|
document.getElementById('back-dashboard').href = P+'/';
|
||||||
|
document.getElementById('back-console').href = P+'/console';
|
||||||
|
|
||||||
|
var sortKey='total_cost', sortDir='desc';
|
||||||
|
var lastRows=[];
|
||||||
|
var tickerFilter=null; // selected ticker for filtering the table
|
||||||
|
var lastQuotes={}; // ticker → quote (price, day_change_pct)
|
||||||
|
var lastBasket=[]; // basket rows aggregated from lastRows
|
||||||
|
var signalMap=null; // leaflet map instance for the per-ticker view
|
||||||
|
|
||||||
|
function clearChildren(el){ while(el.firstChild) el.removeChild(el.firstChild); }
|
||||||
|
function fmt$(n){
|
||||||
|
if(n>=1e9) return '$'+(n/1e9).toFixed(2)+'B';
|
||||||
|
if(n>=1e6) return '$'+(n/1e6).toFixed(1)+'M';
|
||||||
|
if(n>=1e3) return '$'+(n/1e3).toFixed(0)+'K';
|
||||||
|
return '$'+Math.round(n||0).toLocaleString();
|
||||||
|
}
|
||||||
|
function costClass(n){
|
||||||
|
if(n>=1e7) return 'cost-band-3';
|
||||||
|
if(n>=1e6) return 'cost-band-2';
|
||||||
|
return 'cost-band-1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(){
|
||||||
|
var search=document.getElementById('q').value.trim();
|
||||||
|
var since=document.getElementById('since').value;
|
||||||
|
var minCost=parseInt(document.getElementById('min-cost').value,10);
|
||||||
|
document.getElementById('meta').textContent='Loading…';
|
||||||
|
var host=document.getElementById('result'); clearChildren(host);
|
||||||
|
var loading=document.createElement('div'); loading.className='loading';
|
||||||
|
loading.textContent='Aggregating from Chicago Socrata…';
|
||||||
|
host.appendChild(loading);
|
||||||
|
|
||||||
|
fetch(P+'/intelligence/profiler_index',{
|
||||||
|
method:'POST',
|
||||||
|
headers:{'Content-Type':'application/json'},
|
||||||
|
body:JSON.stringify({since:since,min_cost:minCost,search:search,limit:200})
|
||||||
|
}).then(function(r){return r.json()}).then(function(d){
|
||||||
|
lastRows = d.contractors||[];
|
||||||
|
document.getElementById('meta').textContent=lastRows.length+' contractors · '+(d.duration_ms||0)+'ms';
|
||||||
|
// Build the ticker basket from the surfaced rows
|
||||||
|
buildBasket();
|
||||||
|
var totalCost = lastRows.reduce(function(s,r){return s+(r.total_cost||0)},0);
|
||||||
|
var totalPermits = lastRows.reduce(function(s,r){return s+(r.permits||0)},0);
|
||||||
|
var sumDiv=document.getElementById('summary');
|
||||||
|
sumDiv.style.display='block';
|
||||||
|
clearChildren(sumDiv);
|
||||||
|
var b1=document.createElement('b'); b1.textContent=lastRows.length.toLocaleString();
|
||||||
|
sumDiv.appendChild(b1);
|
||||||
|
sumDiv.appendChild(document.createTextNode(' contractors · '));
|
||||||
|
var b2=document.createElement('b'); b2.textContent=totalPermits.toLocaleString();
|
||||||
|
sumDiv.appendChild(b2);
|
||||||
|
sumDiv.appendChild(document.createTextNode(' total permits · '));
|
||||||
|
var b3=document.createElement('b'); b3.textContent=fmt$(totalCost);
|
||||||
|
sumDiv.appendChild(b3);
|
||||||
|
sumDiv.appendChild(document.createTextNode(' aggregate value · since '+(d.since||'?')+' · min permit cost '+fmt$(d.min_cost||0)));
|
||||||
|
render();
|
||||||
|
}).catch(function(e){
|
||||||
|
document.getElementById('meta').textContent='error';
|
||||||
|
var host=document.getElementById('result'); clearChildren(host);
|
||||||
|
var er=document.createElement('div'); er.className='empty'; er.style.color='#f85149';
|
||||||
|
er.textContent='Profiler index error: '+e.message;
|
||||||
|
host.appendChild(er);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate every public ticker the profiler index surfaced, with a
|
||||||
|
// kind hierarchy (exact > direct > parent > associated) and the count
|
||||||
|
// of contractors each ticker is attributed to. Then fetch live quotes
|
||||||
|
// in one batch and render the scrolling basket.
|
||||||
|
function buildBasket(){
|
||||||
|
var byTicker = {};
|
||||||
|
lastRows.forEach(function(r){
|
||||||
|
var ts = (r.tickers && r.tickers.direct ? r.tickers.direct : []).concat(r.tickers && r.tickers.associated ? r.tickers.associated : []);
|
||||||
|
ts.forEach(function(t){
|
||||||
|
if(!t || !t.ticker) return;
|
||||||
|
if(!byTicker[t.ticker]) byTicker[t.ticker] = {ticker:t.ticker, kinds:new Set(), count:0, contractors:[], matched_name:t.matched_name||t.partner_name||null};
|
||||||
|
byTicker[t.ticker].kinds.add(t.via);
|
||||||
|
byTicker[t.ticker].count++;
|
||||||
|
if(byTicker[t.ticker].contractors.length < 5) byTicker[t.ticker].contractors.push(r.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var basketRows = Object.values(byTicker)
|
||||||
|
.map(function(b){
|
||||||
|
// Pick a single 'kind' for the bar color: direct/exact wins, then parent, then associated.
|
||||||
|
var k = b.kinds.has('exact')?'exact':b.kinds.has('direct')?'direct':b.kinds.has('parent')?'parent':b.kinds.has('associated')?'associated':'mixed';
|
||||||
|
if(b.kinds.size>1 && (b.kinds.has('exact')||b.kinds.has('direct')) && b.kinds.has('associated')) k='mixed';
|
||||||
|
return Object.assign({}, b, {kinds:Array.from(b.kinds), kind:k});
|
||||||
|
})
|
||||||
|
.sort(function(a,b){return b.count - a.count});
|
||||||
|
var wrap = document.getElementById('basket-wrap');
|
||||||
|
var track = document.getElementById('basket');
|
||||||
|
clearChildren(track);
|
||||||
|
if(!basketRows.length){
|
||||||
|
wrap.style.display='block';
|
||||||
|
var emp=document.createElement('div'); emp.className='basket-empty';
|
||||||
|
emp.textContent='No public issuers in this view. Try a wider filter or "since 2020" history.';
|
||||||
|
track.appendChild(emp);
|
||||||
|
document.getElementById('bk-count').textContent='0';
|
||||||
|
document.getElementById('bk-meta').textContent='';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wrap.style.display='block';
|
||||||
|
document.getElementById('bk-count').textContent=basketRows.length;
|
||||||
|
document.getElementById('bk-meta').textContent='loading prices…';
|
||||||
|
// Render shells immediately, then fill in prices when the batch returns
|
||||||
|
basketRows.forEach(function(b){
|
||||||
|
var card=document.createElement('div'); card.className='bk-card no-quote';
|
||||||
|
card.dataset.ticker=b.ticker;
|
||||||
|
var bar=document.createElement('div'); bar.className='kind-bar '+b.kind; card.appendChild(bar);
|
||||||
|
var tk=document.createElement('div'); tk.className='tk'; tk.textContent=b.ticker; card.appendChild(tk);
|
||||||
|
var px=document.createElement('div'); px.className='px'; px.textContent='—'; card.appendChild(px);
|
||||||
|
var ch=document.createElement('div'); ch.className='ch flat'; ch.textContent=' '; card.appendChild(ch);
|
||||||
|
var meta=document.createElement('div'); meta.className='meta';
|
||||||
|
meta.textContent=b.count+' attribution'+(b.count===1?'':'s')+' · '+b.kinds.join('+');
|
||||||
|
card.appendChild(meta);
|
||||||
|
card.title=(b.matched_name||b.ticker)+'\n'+b.contractors.slice(0,5).join('\n')+(b.count>5?'\n…':'');
|
||||||
|
card.onclick=function(){
|
||||||
|
tickerFilter = (tickerFilter===b.ticker) ? null : b.ticker;
|
||||||
|
Array.prototype.forEach.call(track.children, function(c){
|
||||||
|
c.classList.toggle('selected', c.dataset && c.dataset.ticker===tickerFilter);
|
||||||
|
});
|
||||||
|
document.getElementById('bk-clear').style.display = tickerFilter ? 'inline' : 'none';
|
||||||
|
showSignalMap(tickerFilter);
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
track.appendChild(card);
|
||||||
|
});
|
||||||
|
lastBasket = basketRows;
|
||||||
|
// Update the hero panel right away with what we know without prices
|
||||||
|
updateThesisMetrics();
|
||||||
|
// Batch-fetch quotes and update each card + thesis
|
||||||
|
fetch(P+'/intelligence/ticker_quotes',{
|
||||||
|
method:'POST',headers:{'Content-Type':'application/json'},
|
||||||
|
body:JSON.stringify({tickers:basketRows.map(function(b){return b.ticker})})
|
||||||
|
}).then(function(r){return r.json()}).then(function(qd){
|
||||||
|
var quotes=qd.quotes||{};
|
||||||
|
lastQuotes = quotes;
|
||||||
|
document.getElementById('bk-meta').textContent='quotes via Stooq · '+(qd.duration_ms||0)+'ms';
|
||||||
|
Array.prototype.forEach.call(track.children, function(card){
|
||||||
|
var t=card.dataset.ticker; var q=quotes[t];
|
||||||
|
if(!q || !q.price) return;
|
||||||
|
card.classList.remove('no-quote');
|
||||||
|
var px=card.querySelector('.px'); px.textContent='$'+q.price.toFixed(2);
|
||||||
|
var ch=card.querySelector('.ch');
|
||||||
|
if(q.day_change_pct==null){ ch.textContent='close '+(q.price_date||''); ch.className='ch flat'; }
|
||||||
|
else if(q.day_change_pct>=0){ ch.textContent='+'+q.day_change_pct.toFixed(2)+'%'; ch.className='ch up'; }
|
||||||
|
else { ch.textContent=q.day_change_pct.toFixed(2)+'%'; ch.className='ch down'; }
|
||||||
|
});
|
||||||
|
updateThesisMetrics();
|
||||||
|
}).catch(function(){
|
||||||
|
document.getElementById('bk-meta').textContent='quote fetch failed';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the Building Activity Index and update the hero panel.
|
||||||
|
// BAI = attribution-weighted day-change % across surfaced issuers.
|
||||||
|
// "Indexed build value" = total dollars of permits attributable to
|
||||||
|
// any public issuer in this view (sum across attributing contractors).
|
||||||
|
// "Network depth" = issuer count + total attributions.
|
||||||
|
function updateThesisMetrics(){
|
||||||
|
if(!lastBasket.length){
|
||||||
|
document.getElementById('bai-value').textContent='—';
|
||||||
|
document.getElementById('bai-sub').textContent='awaiting basket data';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// BAI: weighted average of day_change_pct, weight = attribution count.
|
||||||
|
var weightedSum=0, weightTotal=0, contributors=[];
|
||||||
|
lastBasket.forEach(function(b){
|
||||||
|
var q = lastQuotes[b.ticker];
|
||||||
|
if(q && q.day_change_pct!=null){
|
||||||
|
var w = b.count || 1;
|
||||||
|
weightedSum += q.day_change_pct * w;
|
||||||
|
weightTotal += w;
|
||||||
|
contributors.push({ticker:b.ticker, day:q.day_change_pct, weight:w});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var bai = weightTotal>0 ? (weightedSum/weightTotal) : null;
|
||||||
|
var baiEl = document.getElementById('bai-value');
|
||||||
|
var baiSub = document.getElementById('bai-sub');
|
||||||
|
if(bai==null){
|
||||||
|
baiEl.textContent='—'; baiSub.textContent='no quotes settled yet';
|
||||||
|
baiEl.className='bai-value';
|
||||||
|
} else {
|
||||||
|
var sign = bai>=0 ? '+' : '';
|
||||||
|
baiEl.textContent = sign + bai.toFixed(2) + '%';
|
||||||
|
baiEl.className = 'bai-value ' + (bai>=0?'up':'down');
|
||||||
|
contributors.sort(function(a,b){return Math.abs(b.day*b.weight) - Math.abs(a.day*a.weight)});
|
||||||
|
var top = contributors.slice(0,3).map(function(c){return c.ticker+' '+(c.day>=0?'+':'')+c.day.toFixed(1)+'%'}).join(' · ');
|
||||||
|
baiSub.textContent = contributors.length+' issuers contributing · top: '+top;
|
||||||
|
}
|
||||||
|
// Indexed build value
|
||||||
|
var totalCost = 0;
|
||||||
|
lastRows.forEach(function(r){
|
||||||
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||||||
|
if(ts.length>0) totalCost += (r.total_cost||0);
|
||||||
|
});
|
||||||
|
var bav = totalCost>=1e9 ? '$'+(totalCost/1e9).toFixed(2)+'B' : totalCost>=1e6 ? '$'+(totalCost/1e6).toFixed(0)+'M' : '$'+Math.round(totalCost/1e3)+'K';
|
||||||
|
document.getElementById('bav-value').textContent = bav;
|
||||||
|
document.getElementById('bav-sub').textContent = lastBasket.length+' issuers in scope';
|
||||||
|
// Network depth
|
||||||
|
var totalAttrib = lastBasket.reduce(function(s,b){return s + (b.count||0)},0);
|
||||||
|
document.getElementById('net-value').textContent = lastBasket.length + ' / ' + totalAttrib;
|
||||||
|
document.getElementById('net-sub').textContent = 'issuers / attribution edges';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-ticker map: when a ticker is selected, plot the contractor
|
||||||
|
// permit locations attributed to that ticker. Pulls lat/lng for each
|
||||||
|
// attributed contractor from the contractor profile endpoint and
|
||||||
|
// merges. Caches per-ticker so toggling is instant.
|
||||||
|
var mapCache = {};
|
||||||
|
function showSignalMap(ticker){
|
||||||
|
var wrap=document.getElementById('signal-map-wrap');
|
||||||
|
if(!ticker){ wrap.classList.remove('active'); if(signalMap){signalMap.remove(); signalMap=null;} return; }
|
||||||
|
wrap.classList.add('active');
|
||||||
|
document.getElementById('signal-map-ticker').textContent = ticker;
|
||||||
|
document.getElementById('signal-map-meta').textContent = 'loading permits…';
|
||||||
|
// Find the contractors attributed to this ticker
|
||||||
|
var attrib = lastRows.filter(function(r){
|
||||||
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||||||
|
return ts.some(function(t){return t.ticker===ticker});
|
||||||
|
});
|
||||||
|
if(!attrib.length){
|
||||||
|
document.getElementById('signal-map-meta').textContent='no attributed contractors';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Use the contractor_profile endpoint per attributed contractor (cap at 6)
|
||||||
|
// to pull their geocoded permits, then render. Cached per ticker.
|
||||||
|
if(mapCache[ticker]){
|
||||||
|
drawSignalMap(ticker, mapCache[ticker]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var names = attrib.slice(0,6).map(function(r){return r.name});
|
||||||
|
Promise.all(names.map(function(n){
|
||||||
|
return fetch(P+'/intelligence/contractor_profile',{
|
||||||
|
method:'POST',headers:{'Content-Type':'application/json'},
|
||||||
|
body:JSON.stringify({name:n})
|
||||||
|
}).then(function(r){return r.json()}).then(function(d){
|
||||||
|
var perms = (d.history && d.history.recent_permits) || [];
|
||||||
|
return perms.filter(function(p){return p.lat&&p.lng}).map(function(p){
|
||||||
|
return Object.assign({contractor:n}, p);
|
||||||
|
});
|
||||||
|
}).catch(function(){return []});
|
||||||
|
})).then(function(arrs){
|
||||||
|
var all = arrs.reduce(function(a,b){return a.concat(b)},[]);
|
||||||
|
mapCache[ticker] = all;
|
||||||
|
drawSignalMap(ticker, all);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function drawSignalMap(ticker, permits){
|
||||||
|
if(signalMap){ signalMap.remove(); signalMap=null; }
|
||||||
|
if(!permits.length){
|
||||||
|
document.getElementById('signal-map-meta').textContent='0 geocoded permits across attributed contractors';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('signal-map-meta').textContent = permits.length + ' geocoded permits across ' + new Set(permits.map(function(p){return p.contractor})).size + ' contractors';
|
||||||
|
signalMap = L.map('signal-map',{zoomControl:true, attributionControl:false}).setView([41.88,-87.63], 11);
|
||||||
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{maxZoom:19}).addTo(signalMap);
|
||||||
|
var bounds=[];
|
||||||
|
var maxCost = Math.max.apply(null, permits.map(function(p){return Number(p.cost)||1}));
|
||||||
|
permits.forEach(function(p){
|
||||||
|
var c=Number(p.cost)||0;
|
||||||
|
var radius = 4 + (c/maxCost)*14;
|
||||||
|
var color = c>=1000000?'#f85149':c>=100000?'#d29922':'#3fb950';
|
||||||
|
var marker = L.circleMarker([p.lat,p.lng],{radius:radius,color:color,weight:1,fillOpacity:0.55});
|
||||||
|
var pop=document.createElement('div');
|
||||||
|
pop.style.cssText='font-family:ui-monospace,monospace;font-size:11px;color:#0a0d12;min-width:200px';
|
||||||
|
var top=document.createElement('div'); top.style.cssText='font-weight:700;margin-bottom:3px;color:#1f6feb';
|
||||||
|
top.textContent=ticker+' attribution';
|
||||||
|
pop.appendChild(top);
|
||||||
|
var con=document.createElement('div'); con.textContent=p.contractor; con.style.fontWeight='600';
|
||||||
|
pop.appendChild(con);
|
||||||
|
var meta=document.createElement('div'); meta.style.color='#545d68';
|
||||||
|
meta.textContent='$'+c.toLocaleString()+' · '+(p.date||'?')+' · '+(p.work_type||'?');
|
||||||
|
pop.appendChild(meta);
|
||||||
|
var addr=document.createElement('div'); addr.style.color='#545d68';
|
||||||
|
addr.textContent=p.address||'?';
|
||||||
|
pop.appendChild(addr);
|
||||||
|
marker.bindPopup(pop);
|
||||||
|
marker.addTo(signalMap);
|
||||||
|
bounds.push([p.lat,p.lng]);
|
||||||
|
});
|
||||||
|
if(bounds.length>1) signalMap.fitBounds(bounds,{padding:[28,28]});
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(){
|
||||||
|
var host=document.getElementById('result');
|
||||||
|
clearChildren(host);
|
||||||
|
// Apply ticker filter if set: keep only rows whose tickers include the selected one
|
||||||
|
var pool = tickerFilter ? lastRows.filter(function(r){
|
||||||
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||||||
|
return ts.some(function(t){return t.ticker===tickerFilter});
|
||||||
|
}) : lastRows;
|
||||||
|
if(!pool.length){
|
||||||
|
var emp=document.createElement('div'); emp.className='empty';
|
||||||
|
emp.textContent='No contractors match the current filter.';
|
||||||
|
host.appendChild(emp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var rows = pool.slice().sort(function(a,b){
|
||||||
|
var av=a[sortKey], bv=b[sortKey];
|
||||||
|
if(typeof av==='string'){ av=(av||'').toUpperCase(); bv=(bv||'').toUpperCase(); }
|
||||||
|
if(av<bv) return sortDir==='asc'?-1:1;
|
||||||
|
if(av>bv) return sortDir==='asc'?1:-1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
var t=document.createElement('table');
|
||||||
|
var thead=document.createElement('thead'); var hr=document.createElement('tr');
|
||||||
|
var cols=[
|
||||||
|
{k:'name', label:'Contractor'},
|
||||||
|
{k:'permits', label:'Permits', right:true},
|
||||||
|
{k:'total_cost', label:'Total Value', right:true},
|
||||||
|
{k:'last_filed', label:'Last Filed', right:true},
|
||||||
|
{k:'roles', label:'Listed As'},
|
||||||
|
];
|
||||||
|
cols.forEach(function(c){
|
||||||
|
var h=document.createElement('th');
|
||||||
|
h.textContent=c.label;
|
||||||
|
if(c.right) h.style.textAlign='right';
|
||||||
|
if(sortKey===c.k){
|
||||||
|
var ar=document.createElement('span'); ar.className='arrow';
|
||||||
|
ar.textContent = sortDir==='asc' ? '▲' : '▼';
|
||||||
|
h.appendChild(ar);
|
||||||
|
}
|
||||||
|
h.onclick=function(){
|
||||||
|
if(sortKey===c.k) sortDir = sortDir==='asc' ? 'desc' : 'asc';
|
||||||
|
else { sortKey=c.k; sortDir = (c.k==='name') ? 'asc' : 'desc'; }
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
hr.appendChild(h);
|
||||||
|
});
|
||||||
|
thead.appendChild(hr); t.appendChild(thead);
|
||||||
|
|
||||||
|
var tb=document.createElement('tbody');
|
||||||
|
rows.forEach(function(r){
|
||||||
|
var tr=document.createElement('tr');
|
||||||
|
var ntd=document.createElement('td'); ntd.className='name';
|
||||||
|
var a=document.createElement('a');
|
||||||
|
a.href = P+'/contractor?name='+encodeURIComponent(r.name);
|
||||||
|
a.target='_blank'; a.rel='noopener';
|
||||||
|
a.textContent = r.name;
|
||||||
|
ntd.appendChild(a);
|
||||||
|
// Ticker association pills — direct (green) = the contractor is a
|
||||||
|
// public issuer; parent (amber) = subsidiary of a public parent;
|
||||||
|
// associated (blue) = co-appears on permits with a public entity.
|
||||||
|
// Shows the correlation indicator J described — when Bob's Electric
|
||||||
|
// works permits with Target, TGT renders here as associated.
|
||||||
|
var t = r.tickers || {direct:[], associated:[]};
|
||||||
|
var allTk = (t.direct||[]).concat(t.associated||[]);
|
||||||
|
if(allTk.length){
|
||||||
|
var trk = document.createElement('div'); trk.className='tickers';
|
||||||
|
allTk.forEach(function(x){
|
||||||
|
var p = document.createElement('span');
|
||||||
|
p.className = 'ticker-pill ' + (x.via||'direct');
|
||||||
|
p.textContent = x.ticker;
|
||||||
|
// Tooltip shows the full reason path
|
||||||
|
var hint = x.via === 'associated'
|
||||||
|
? 'Associated via co-permits with '+x.partner_name+' ('+(x.co_permits||0)+' shared permits)' + (x.matched_name ? ' — '+x.matched_name : '')
|
||||||
|
: x.via === 'parent'
|
||||||
|
? 'Subsidiary of '+(x.matched_name||x.ticker) + (x.exchange ? ' · '+x.exchange : '')
|
||||||
|
: 'Direct match: '+(x.matched_name||r.name);
|
||||||
|
p.title = hint;
|
||||||
|
trk.appendChild(p);
|
||||||
|
});
|
||||||
|
ntd.appendChild(trk);
|
||||||
|
}
|
||||||
|
tr.appendChild(ntd);
|
||||||
|
var ptd=document.createElement('td'); ptd.className='right';
|
||||||
|
ptd.textContent=(r.permits||0).toLocaleString();
|
||||||
|
tr.appendChild(ptd);
|
||||||
|
var ctd=document.createElement('td'); ctd.className='right '+costClass(r.total_cost||0);
|
||||||
|
ctd.textContent=fmt$(r.total_cost||0);
|
||||||
|
tr.appendChild(ctd);
|
||||||
|
var ltd=document.createElement('td'); ltd.className='right';
|
||||||
|
ltd.textContent=(r.last_filed||'').slice(0,10) || '—';
|
||||||
|
tr.appendChild(ltd);
|
||||||
|
var rtd=document.createElement('td'); rtd.className='role';
|
||||||
|
(r.roles||[]).forEach(function(role){
|
||||||
|
var pill=document.createElement('span'); pill.className='pill'; pill.textContent=role;
|
||||||
|
rtd.appendChild(pill);
|
||||||
|
});
|
||||||
|
tr.appendChild(rtd);
|
||||||
|
tb.appendChild(tr);
|
||||||
|
});
|
||||||
|
t.appendChild(tb);
|
||||||
|
host.appendChild(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sDeb;
|
||||||
|
document.getElementById('q').addEventListener('input',function(){
|
||||||
|
clearTimeout(sDeb);
|
||||||
|
sDeb=setTimeout(load,400);
|
||||||
|
});
|
||||||
|
document.getElementById('since').addEventListener('change',load);
|
||||||
|
document.getElementById('min-cost').addEventListener('change',load);
|
||||||
|
document.getElementById('bk-clear').addEventListener('click',function(){
|
||||||
|
tickerFilter=null;
|
||||||
|
document.getElementById('bk-clear').style.display='none';
|
||||||
|
Array.prototype.forEach.call(document.querySelectorAll('.bk-card.selected'), function(c){c.classList.remove('selected')});
|
||||||
|
showSignalMap(null);
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('load',load);
|
||||||
|
</script>
|
||||||
|
</body></html>
|
||||||
@ -81,6 +81,7 @@ pre{background:#161b22;border:1px solid #171d27;border-radius:8px;padding:14px 1
|
|||||||
<nav>
|
<nav>
|
||||||
<a href=".">Dashboard</a>
|
<a href=".">Dashboard</a>
|
||||||
<a href="console">Walkthrough</a>
|
<a href="console">Walkthrough</a>
|
||||||
|
<a href="profiler">Profiler</a>
|
||||||
<a href="proof" class="active">Architecture</a>
|
<a href="proof" class="active">Architecture</a>
|
||||||
<a href="spec">Spec</a>
|
<a href="spec">Spec</a>
|
||||||
<a href="onboard">Onboard</a>
|
<a href="onboard">Onboard</a>
|
||||||
@ -95,138 +96,137 @@ pre{background:#161b22;border:1px solid #171d27;border-radius:8px;padding:14px 1
|
|||||||
<div class="chapter">
|
<div class="chapter">
|
||||||
<div class="num">Chapter 1</div>
|
<div class="num">Chapter 1</div>
|
||||||
<h2>Receipts, not promises</h2>
|
<h2>Receipts, not promises</h2>
|
||||||
<div class="lede">Every test below ran live against the real gateway when you loaded this page. Sub-100ms SQL on multi-million-row Parquet, hybrid search with playbook boost applied. No fixtures. If a test fails, you'll see ✗.</div>
|
<div class="lede">Every test below ran live against the real gateway when you loaded this page. Sub-100ms SQL on multi-million-row Parquet, hybrid search with playbook boost applied, public-issuer attribution computed from this view. No fixtures. If a test fails, you'll see ✗.</div>
|
||||||
<div id="ch1-tests"><div class="loading">Running tests…</div></div>
|
<div id="ch1-tests"><div class="loading">Running tests…</div></div>
|
||||||
|
<div id="ch1-live" style="margin-top:14px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chapter">
|
<div class="chapter">
|
||||||
<div class="num">Chapter 2</div>
|
<div class="num">Chapter 2</div>
|
||||||
<h2>Architecture — 13 crates, one object store, one local AI runtime</h2>
|
<h2>Architecture — 15 crates, one object store, a 5-provider model fleet</h2>
|
||||||
<div class="lede">Request flows top to bottom. Every node is independently swappable. Every line is a real HTTP or gRPC hop that you can trace with <code>tcpdump</code>.</div>
|
<div class="lede">Gateway is a drop-in OpenAI-compatible middleware. Any consumer that speaks the OpenAI Chat Completions shape — agent SDKs, IDE plugins, custom apps — points at <code>localhost:3100/v1</code> and gets routing, audit, and the full memory substrate behind every call. The model side has 5 providers and 40+ frontier models reachable via one OpenCode key. The data side stays Rust-first.</div>
|
||||||
<div class="card accent-b">
|
<div class="card accent-b">
|
||||||
<pre> HTTP :3100 + gRPC :3101
|
<pre> OpenAI SDK consumers MCP clients Browser UI (Bun :3700)
|
||||||
│
|
│ │ │
|
||||||
┌───────▼───────┐
|
└──────────────────────────┼──────────────────────────┘
|
||||||
│ gateway │ Rust · Axum · routing, CORS, auth, tools
|
▼
|
||||||
└───────┬───────┘
|
┌──────────────────────────────┐
|
||||||
┌────────────┬───────────┼───────────┬────────────┐
|
│ gateway :3100 /v1/* │ Rust · Axum
|
||||||
│ │ │ │ │
|
│ OpenAI-compat drop-in │ smart provider routing
|
||||||
┌────▼───┐ ┌────▼───┐ ┌────▼───┐ ┌────▼───┐ ┌────▼───┐
|
│ /v1/chat /v1/mode /iterate │ cost telemetry, Langfuse
|
||||||
│catalog │ │ query │ │ vector │ │ ingest │ │aibridge│
|
└──────────┬───────────────────┘
|
||||||
│ d │ │ d │ │ d │ │ d │ │ │
|
┌─────────┬───────────────┼───────────────┬──────────┐
|
||||||
└────┬───┘ └────┬───┘ └────┬───┘ └────┬───┘ └────┬───┘
|
│ │ │ │ │
|
||||||
│ │ │ │ │
|
┌────▼───┐ ┌───▼────┐ ┌─────▼──────┐ ┌─────▼─────┐ ┌──▼──────┐
|
||||||
└────────────┴───────────┼───────────┴────────────┘
|
│catalog │ │ query │ │ vector │ │ ingest │ │aibridge │
|
||||||
▼
|
│ d │ │ d │ │ d │ │ d │ │ │
|
||||||
┌─────────────────┐
|
│idempot │ │DataFus │ │HNSW · Lance│ │CSV PDF SQL│ │provider │
|
||||||
│ object storage │ Parquet files (local / S3)
|
│schema │ │delta │ │playbook+ │ │auto-PII │ │adapters │
|
||||||
└─────────────────┘
|
│fingerp │ │MemTabl │ │pathway mem │ │schema fp │ │5 active │
|
||||||
▲
|
└────┬───┘ └───┬────┘ └─────┬──────┘ └─────┬─────┘ └──┬──────┘
|
||||||
│
|
└─────────┴────────────────┼────────────────┴─────────┘
|
||||||
┌───────┴────────┐
|
▼
|
||||||
│ Python sidecar │ FastAPI → Ollama
|
┌──────────────────┐
|
||||||
│ (aibridge) │ local models only
|
│ object storage │ Parquet · MinIO · S3-compat
|
||||||
└────────────────┘</pre>
|
└──────────────────┘
|
||||||
|
▲
|
||||||
|
│
|
||||||
|
┌───────────────┴────────────────┐
|
||||||
|
│ validator · journald │ schema/PII/policy gates
|
||||||
|
│ (Phase 43) · (audit log) │ + append-only mutations
|
||||||
|
└────────────────────────────────┘
|
||||||
|
|
||||||
|
Provider fleet (config/providers.toml):
|
||||||
|
ollama localhost:3200 local Ollama → qwen3.5, gemma2
|
||||||
|
ollama_cloud ollama.com gpt-oss:120b, qwen3-coder:480b,
|
||||||
|
deepseek-v3.1:671b, kimi-k2:1t,
|
||||||
|
mistral-large-3:675b, qwen3.5:397b
|
||||||
|
openrouter openrouter.ai/api/v1 343 models — paid + free rescue
|
||||||
|
opencode opencode.ai/zen/v1 40 models · ONE sk-* key reaches
|
||||||
|
Claude Opus 4.7, GPT-5.5-pro,
|
||||||
|
Gemini 3.1-pro, Kimi K2.6, GLM 5.1,
|
||||||
|
DeepSeek, Qwen, MiniMax, free tier
|
||||||
|
kimi api.kimi.com/coding/v1 direct Kimi For Coding (TOS-clean)</pre>
|
||||||
</div>
|
</div>
|
||||||
<h3>Per-crate responsibility</h3>
|
<h3>Per-crate responsibility (15 crates)</h3>
|
||||||
<table class="plain">
|
<table class="plain">
|
||||||
<thead><tr><th>Crate</th><th>Role</th><th>Path</th></tr></thead>
|
<thead><tr><th>Crate</th><th>Role</th><th>Path</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>shared</td><td>Types, errors, Arrow helpers, PII detection, secrets provider</td><td>crates/shared/</td></tr>
|
<tr><td>shared</td><td>Types, errors, Arrow helpers, PII detection, secrets provider, model_matrix</td><td>crates/shared/</td></tr>
|
||||||
<tr><td>storaged</td><td>object_store I/O, BucketRegistry (multi-bucket), AppendLog, ErrorJournal</td><td>crates/storaged/</td></tr>
|
<tr><td>storaged</td><td>object_store I/O, BucketRegistry, AppendLog, ErrorJournal, federation_service</td><td>crates/storaged/</td></tr>
|
||||||
<tr><td>catalogd</td><td>Metadata authority — manifests, views, tombstones, profiles, schema fingerprints</td><td>crates/catalogd/</td></tr>
|
<tr><td>catalogd</td><td>Manifests, views (incl. PII-safe view layer), tombstones, profiles, schema fingerprints, register-idempotency (ADR-020)</td><td>crates/catalogd/</td></tr>
|
||||||
<tr><td>queryd</td><td>DataFusion SQL engine, MemTable cache, delta merge-on-read, compaction</td><td>crates/queryd/</td></tr>
|
<tr><td>queryd</td><td>DataFusion SQL, MemTable cache, delta merge-on-read, compaction, truth gate (ADR-021)</td><td>crates/queryd/</td></tr>
|
||||||
<tr><td>ingestd</td><td>CSV/JSON/PDF(+OCR)/Postgres/MySQL ingest, cron schedules, auto-PII</td><td>crates/ingestd/</td></tr>
|
<tr><td>ingestd</td><td>CSV/JSON/PDF(+OCR)/Postgres/MySQL ingest, cron schedules, auto-PII flagging</td><td>crates/ingestd/</td></tr>
|
||||||
<tr><td>vectord</td><td>Embeddings as Parquet, HNSW, trial system, autotune agent, playbook_memory</td><td>crates/vectord/</td></tr>
|
<tr><td>vectord</td><td>Embeddings as Parquet, HNSW, trial system, autotune, playbook_memory + pathway_memory (ADR-021 semantic-correctness layer)</td><td>crates/vectord/</td></tr>
|
||||||
<tr><td>vectord-lance</td><td>Firewall crate — Lance 4.0 + Arrow 57 isolated from main Arrow 55</td><td>crates/vectord-lance/</td></tr>
|
<tr><td>vectord-lance</td><td>Firewall crate — Lance 4.0 + Arrow 57 isolated from main Arrow 55</td><td>crates/vectord-lance/</td></tr>
|
||||||
<tr><td>journald</td><td>Append-only mutation event log for time-travel & audit</td><td>crates/journald/</td></tr>
|
<tr><td>journald</td><td>Append-only mutation event log for time-travel + audit</td><td>crates/journald/</td></tr>
|
||||||
<tr><td>aibridge</td><td>Rust↔Python sidecar, Ollama HTTP client, VRAM introspection</td><td>crates/aibridge/</td></tr>
|
<tr><td>truth</td><td>File-backed rule store; <code>evaluate(task_class, ctx) → Vec<RuleOutcome></code> (ADR-021)</td><td>crates/truth/</td></tr>
|
||||||
<tr><td>gateway</td><td>Axum HTTP :3100 + gRPC :3101, middleware, tools registry</td><td>crates/gateway/</td></tr>
|
<tr><td>aibridge</td><td>Rust↔Python sidecar, Ollama client, ProviderAdapter trait, /v1/chat router</td><td>crates/aibridge/</td></tr>
|
||||||
<tr><td>ui</td><td>Dioxus WASM internal developer UI</td><td>crates/ui/</td></tr>
|
<tr><td>gateway</td><td>Axum HTTP :3100 + gRPC :3101, OpenAI-compat /v1/*, mode runner, validator, iterate loop, cost telemetry, Langfuse + observer fan-out</td><td>crates/gateway/</td></tr>
|
||||||
<tr><td>mcp-server</td><td>Bun TypeScript recruiter-facing app (this server)</td><td>mcp-server/</td></tr>
|
<tr><td>validator</td><td>Phase 43 — schema / completeness / consistency / policy gates over LLM outputs (FillValidator, EmailValidator, ParquetWorkerLookup)</td><td>crates/validator/</td></tr>
|
||||||
|
<tr><td>ui</td><td>Dioxus WASM internal developer UI (separate from this Bun-served public UI)</td><td>crates/ui/</td></tr>
|
||||||
|
<tr><td>mcp-server</td><td>Bun TypeScript public-facing app + MCP tool surface — what you're reading right now</td><td>mcp-server/</td></tr>
|
||||||
|
<tr><td>auditor</td><td>External claim-vs-diff verifier on PRs · Kimi K2.6 ↔ Haiku 4.5 cross-lineage alternation, Opus 4.7 auto-promote on diffs >100k chars</td><td>auditor/</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="ref"><strong>Source:</strong> git.agentview.dev/profit/lakehouse · <strong>ADRs:</strong> docs/DECISIONS.md (currently 20 records)</div>
|
<div class="ref"><strong>Source:</strong> git.agentview.dev/profit/lakehouse · branch <code>scrum/auto-apply-19814</code> · tag <code>distillation-v1.0.0</code> at commit <code>e7636f2</code> (frozen substrate) · <strong>ADRs:</strong> docs/DECISIONS.md (currently 21 records)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chapter">
|
<div class="chapter">
|
||||||
<div class="num">Chapter 3</div>
|
<div class="num">Chapter 3</div>
|
||||||
<h2>Dual-agent recursive consensus loop</h2>
|
<h2>The model fleet — 9-rung ladder, N=3 consensus, cross-lineage audit</h2>
|
||||||
<div class="lede">The system we use to execute staffing fills is a dual-agent recursive protocol. Two agents with distinct roles iterate against a shared log until one of three terminal states is reached. It is deterministic in structure, stochastic in content, and verifiable through the per-run log artifact.</div>
|
<div class="lede">No single model owns the answer. Every consequential call is structured: the right tier picks up first, fallback rungs catch what fails, parallel runs vote, and an independent auditor of a different model lineage checks the result against the diff. The protocol is deterministic; the inference is stochastic; every step writes a receipt.</div>
|
||||||
<h3>Agents and protocol</h3>
|
|
||||||
<div class="card accent-a">
|
|
||||||
<pre> task in
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌───────────────────────────────────────────────────────────┐
|
|
||||||
│ EXECUTOR (mistral:latest) │
|
|
||||||
│ ──────────────────────────────────────────────────────── │
|
|
||||||
│ input: task spec + shared log + seen-candidates ledger │
|
|
||||||
│ output: one JSON action per turn │
|
|
||||||
│ · {kind:"plan",steps:[…]} │
|
|
||||||
│ · {kind:"tool_call",tool,args,rationale} │
|
|
||||||
│ · {kind:"propose_done",fills:[N of N]} │
|
|
||||||
└───────────┬───────────────────────────────┬───────────────┘
|
|
||||||
│ tool_call │ propose_done
|
|
||||||
▼ │
|
|
||||||
┌──────────────────────────┐ │
|
|
||||||
│ TOOL DISPATCH │ │
|
|
||||||
│ hybrid_search / sql │ │
|
|
||||||
│ (against live gateway) │ │
|
|
||||||
└──────────┬───────────────┘ │
|
|
||||||
│ result (trimmed, exclusions) │
|
|
||||||
▼ ▼
|
|
||||||
┌───────────────────────────────────────────────────────────┐
|
|
||||||
│ REVIEWER (qwen2.5:latest) │
|
|
||||||
│ ──────────────────────────────────────────────────────── │
|
|
||||||
│ input: task spec + shared log (including tool result) │
|
|
||||||
│ output: {kind:"critique",verdict:"continue|drift| │
|
|
||||||
│ approve_done",notes} │
|
|
||||||
└───────────┬───────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
┌─────┴─────┐
|
|
||||||
▼ ▼ ▼
|
|
||||||
continue drift approve_done + propose_done ⟹ SEAL
|
|
||||||
(next turn) (cap ≈ 3 →
|
|
||||||
hard abort)
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
<div class="ref"><strong>Code:</strong> tests/multi-agent/agent.ts (protocol + prompts) · tests/multi-agent/orchestrator.ts (run loop) · tests/multi-agent/scenario.ts (5-event warehouse week)</div>
|
|
||||||
|
|
||||||
<h3>Why "dual" — role specialization</h3>
|
<h3>The 9-rung cloud-first ladder</h3>
|
||||||
<div class="narr">
|
<div class="card accent-b">
|
||||||
<strong>The executor is an optimist.</strong> Its job is to produce progress: pull candidates, verify SQL, propose consensus. It's instructed to be decisive.
|
<pre> request in
|
||||||
<br><br>
|
│
|
||||||
<strong>The reviewer is a pessimist.</strong> Its job is to catch drift: proposals that don't match the task's geography, fill count, or role. It's authorized to stop the loop.
|
▼
|
||||||
<br><br>
|
┌───────────────────────────────────────────────────────────────────┐
|
||||||
This adversarial separation is cheaper and more deterministic than asking a single model to self-critique. The reviewer has a hard rule: on the turn after a <code>propose_done</code>, it MUST emit either <code>approve_done</code> or <code>drift</code> — it cannot stall with <code>continue</code>.
|
│ attempt 1 ollama_cloud / kimi-k2:1t 1T params · flagship │
|
||||||
|
│ attempt 2 ollama_cloud / qwen3-coder:480b coding specialist │
|
||||||
|
│ attempt 3 ollama_cloud / deepseek-v3.1:671b reasoning │
|
||||||
|
│ attempt 4 ollama_cloud / mistral-large-3:675b deep analysis │
|
||||||
|
│ attempt 5 ollama_cloud / gpt-oss:120b reliable workhorse │
|
||||||
|
│ attempt 6 ollama_cloud / qwen3.5:397b dense final thinker │
|
||||||
|
│ attempt 7 openrouter / openai/gpt-oss-120b:free rescue tier │
|
||||||
|
│ attempt 8 openrouter / google/gemma-3-27b-it:free fastest rescue │
|
||||||
|
│ attempt 9 ollama / qwen3.5:latest last-resort local │
|
||||||
|
└───────────────┬───────────────────────────────────────────────────┘
|
||||||
|
│ isAcceptable() = chars ≥ 3800 ∧ not malformed JSON
|
||||||
|
▼
|
||||||
|
sealed result OR next-rung learning preamble</pre>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="narr">Every rung sees a learning preamble carrying the prior rejection reason. The ladder is the standard scrum/auditor path; for individual <code>/v1/chat</code> calls the caller picks the model directly (or lets the smart-routing default fire).</div>
|
||||||
|
<div class="ref"><strong>Code:</strong> tests/real-world/scrum_master_pipeline.ts <code>const LADDER</code> · config/routing.toml · crates/gateway/src/v1/mode.rs (mode runner)</div>
|
||||||
|
|
||||||
<h3>Why "parallel" — orchestrator can fan out</h3>
|
<h3>N=3 consensus + tie-breaker (auditor inference)</h3>
|
||||||
<div class="narr">
|
|
||||||
<strong>Independent pairs run concurrently.</strong> <code>tests/multi-agent/run_e2e_rated.ts</code> runs two task-specific agent pairs via <code>Promise.all</code>. Ollama serializes inference at the model level, so "parallel" is concurrent orchestration — but the substrate (gateway, queryd, vectord) handles concurrent requests cleanly. Verified in the scenario harness: two contracts sealing simultaneously.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Why "recursive" — each seal feeds the next</h3>
|
|
||||||
<div class="narr">
|
|
||||||
<strong>Consensus does not end at the sealed playbook.</strong> Every sealed playbook is persisted to <code>playbook_memory</code> via <code>POST /vectors/playbook_memory/seed</code>. The next hybrid search for a semantically similar operation consults that memory via <code>compute_boost_for(query_embedding, top_k, base_weight)</code> and re-ranks the candidate pool. The system builds on itself turn over turn, playbook over playbook.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Termination guarantees</h3>
|
|
||||||
<div class="math">
|
<div class="math">
|
||||||
<span class="c">// three paths out, every run has one of these:</span><br>
|
<span class="c">// auditor/checks/inference.ts — every claim audit runs this:</span><br>
|
||||||
sealed = executor.propose_done ∧ reviewer.approve_done ∧ fills.count == target<br>
|
1. Fire the primary reviewer N=3 times in PARALLEL (Promise.all) — wall-clock = single call<br>
|
||||||
abort = consecutive_tool_errors ≥ MAX_TOOL_ERRORS (3) <span class="c">// executor can't form a valid call</span><br>
|
2. Aggregate votes per claim_idx · majority wins<br>
|
||||||
abort = consecutive_drifts ≥ MAX_CONSECUTIVE_DRIFTS (3) <span class="c">// reviewer keeps flagging</span><br>
|
3. On 1-1-1 split → tie-breaker model with <strong>different architecture</strong> (qwen3-coder:480b vs primary gpt-oss/kimi)<br>
|
||||||
abort = turn > MAX_TURNS (12) <span class="c">// no consensus reached in window</span>
|
4. Every disagreement (even when majority resolves) → <code>data/_kb/audit_discrepancies.jsonl</code><br>
|
||||||
|
<br>
|
||||||
|
<span class="c">// Closes the cloud-non-determinism gap: temp=0 isn't actually deterministic in practice</span><br>
|
||||||
|
<span class="c">// across hours; consensus + cross-architecture tie-break stabilizes verdicts.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="narr">Every abort dumps the full log to <code>tests/multi-agent/playbooks/<id>-FAILED.json</code> for forensic review. No consensus is ever implicit.</div>
|
|
||||||
|
<h3>Auditor cross-lineage — Kimi ↔ Haiku ↔ Opus</h3>
|
||||||
|
<div class="narr">Every push to PR #11 triggers <code>auditor/audit.ts</code> within ~90s. To prevent a single model lineage's blind spots from becoming the system's blind spots, audits alternate between Kimi K2.6 (Moonshot) and Haiku 4.5 (Anthropic) by SHA. Diffs over 100k chars auto-promote to Claude Opus 4.7. Per-PR cap of 3 audits with auto-reset on each new head SHA prevents infinite-loop spend. <strong>100% grounding-verified rate</strong> on Haiku 4.5 across the latest 10 findings — pairing different lineages + forcing per-finding grounding kills confabulation.</div>
|
||||||
|
<div class="ref"><strong>Code:</strong> auditor/audit.ts · auditor/checks/inference.ts (N=3) · auditor/checks/kimi_architect.ts · <strong>Verdicts:</strong> data/_auditor/kimi_verdicts/ — read any 11-<sha>.json to inspect a real audit</div>
|
||||||
|
|
||||||
|
<h3>Distillation v1.0.0 — the frozen substrate</h3>
|
||||||
|
<div class="narr">The substrate the auditor and mode runner sit on is tagged at <code>distillation-v1.0.0</code> / commit <code>e7636f2</code>. <strong>145 unit tests pass · 22/22 acceptance invariants · 16/16 audit-full checks · bit-identical reproducibility verified.</strong> The distillation phase exports clean SFT / RAG / preference samples with a multi-layer contamination firewall; the auditor consumes the substrate. The frozen tag means: any future "the system regressed" question has a baseline to bisect against, byte-for-byte.</div>
|
||||||
|
<div class="ref"><strong>Tag:</strong> distillation-v1.0.0 · <strong>Commit:</strong> e7636f2 · <strong>Substrate code:</strong> scripts/distillation/ · auditor/schemas/distillation/ · <strong>Output:</strong> data/_kb/distilled_{facts,procedures,config_hints}.jsonl</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chapter">
|
<div class="chapter">
|
||||||
<div class="num">Chapter 4</div>
|
<div class="num">Chapter 4</div>
|
||||||
<h2>Playbook memory — the compounding feedback loop</h2>
|
<h2>Two memory layers — playbook (worker signal) + pathway (system signal)</h2>
|
||||||
<div class="lede">A CRM stores events. This system turns events into re-ranking signal. Every sealed playbook endorses specific (worker, city, state) tuples. Every failure penalizes them. Every similar future query inherits the signal through cosine similarity.</div>
|
<div class="lede">A CRM stores events. This system turns events into re-ranking signal at two layers. <strong>Playbook memory</strong> compounds worker-level outcomes (who got endorsed, where, when) into per-query boost. <strong>Pathway memory</strong> compounds system-level outcomes (which model + corpus + framing actually solved similar problems) into per-task hot-swap. Both are queryable. Both are auditable. Both compound.</div>
|
||||||
|
|
||||||
|
<h3>Layer 1 — playbook memory (worker + geo signal)</h3>
|
||||||
|
|
||||||
<h3>Seed shape</h3>
|
<h3>Seed shape</h3>
|
||||||
<div class="math">
|
<div class="math">
|
||||||
@ -289,10 +289,82 @@ pre{background:#161b22;border:1px solid #171d27;border-radius:8px;padding:14px 1
|
|||||||
<strong>Beyond "who was endorsed."</strong> <code>POST /vectors/playbook_memory/patterns</code> takes a query, finds top-K similar past playbooks, pulls each endorsed worker's full workers_500k profile, and aggregates shared traits: recurring certifications, skill frequencies, modal archetype, reliability distribution. Returns a <code>discovered_pattern</code> string showing operator-actionable signal the user didn't explicitly query for.
|
<strong>Beyond "who was endorsed."</strong> <code>POST /vectors/playbook_memory/patterns</code> takes a query, finds top-K similar past playbooks, pulls each endorsed worker's full workers_500k profile, and aggregates shared traits: recurring certifications, skill frequencies, modal archetype, reliability distribution. Returns a <code>discovered_pattern</code> string showing operator-actionable signal the user didn't explicitly query for.
|
||||||
</div>
|
</div>
|
||||||
<div class="ref"><strong>Code:</strong> crates/vectord/src/playbook_memory.rs::discover_patterns · <strong>Surfaces:</strong> /vectors/playbook_memory/patterns endpoint, /intelligence/chat response, /intelligence/permit_contracts cards</div>
|
<div class="ref"><strong>Code:</strong> crates/vectord/src/playbook_memory.rs::discover_patterns · <strong>Surfaces:</strong> /vectors/playbook_memory/patterns endpoint, /intelligence/chat response, /intelligence/permit_contracts cards</div>
|
||||||
|
|
||||||
|
<h3>Layer 2 — pathway memory (system-level hot-swap, ADR-021)</h3>
|
||||||
|
<div class="narr">
|
||||||
|
<strong>Pathway memory remembers which approach worked, not just which worker.</strong> Every accepted scrum review writes a <code>PathwayTrace</code> with the full backtrack: file fingerprint, model used, signal class, KB chunks consulted, observer events, semantic flags, bug fingerprints. A new query that fingerprints to the same trace can hot-swap to the prior result without re-running the 9-rung escalation. The 5-factor hot-swap gate is strict: narrow fingerprint match AND audit consensus pass AND replay_count ≥ 3 (probation) AND success_rate ≥ 0.80 AND NOT retired AND vector cosine ≥ 0.90.
|
||||||
|
</div>
|
||||||
|
<div class="math">
|
||||||
|
<span class="c">// Live pathway state (refresh page to recompute):</span><br>
|
||||||
|
<span id="pwm-traces">— traces</span> · <span id="pwm-replays">—</span> successful replays · <span id="pwm-rate">—</span> reuse rate<br>
|
||||||
|
<span class="c">// 88 / 11/11 / 100% as of 2026-04-27 — probation gate crossed</span>
|
||||||
|
</div>
|
||||||
|
<div class="ref"><strong>Code:</strong> crates/vectord/src/pathway_memory.rs · <strong>Endpoints:</strong> /vectors/pathway/insert · /query · /record_replay · /stats · /bug_fingerprints · <strong>Spec:</strong> docs/DECISIONS.md ADR-021 — Semantic-correctness matrix layer</div>
|
||||||
|
|
||||||
|
<h3>What both memory layers feed (besides search)</h3>
|
||||||
|
<div class="narr">
|
||||||
|
Both layers also feed the <strong>per-staffer hot-swap index</strong> (Chapter 5) and the <strong>Construction Activity Signal Engine</strong> (Chapter 6). One memory model, surfaced three different ways at the request boundary depending on who's asking.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chapter">
|
<div class="chapter">
|
||||||
<div class="num">Chapter 5</div>
|
<div class="num">Chapter 5</div>
|
||||||
|
<h2>Per-staffer hot-swap — same corpus, different relevance gradient</h2>
|
||||||
|
<div class="lede">Maria runs Chicago. Devon runs Indianapolis. Aisha runs Wisconsin/Michigan. They share one corpus, but the search results, the recurring-skill patterns, and the playbook context all reshape to whoever is acting. Same query "forklift operators" returns 89 IN workers when Devon's acting, 16 WI when Aisha's, 167 IL when Maria's. The MEMORY panel relabels itself with the active coordinator's name.</div>
|
||||||
|
|
||||||
|
<h3>What scopes per staffer</h3>
|
||||||
|
<div class="math">
|
||||||
|
<span class="c">// On every /intelligence/chat call:</span><br>
|
||||||
|
if (b.staffer_id) {<br>
|
||||||
|
const staffer = lookupStaffer(b.staffer_id);<br>
|
||||||
|
<span class="c">// 1. Default state filter to staffer territory unless caller pinned one</span><br>
|
||||||
|
if (!explicitState) filters.push(`state = '${staffer.territory.state}'`);<br>
|
||||||
|
<span class="c">// 2. Default playbook-pattern geo to staffer's primary city/state</span><br>
|
||||||
|
cityForPatterns = staffer.territory.cities[0];<br>
|
||||||
|
stateForPatterns = staffer.territory.state;<br>
|
||||||
|
<span class="c">// 3. Surface staffer.name back so the UI can relabel MEMORY → MARIA'S MEMORY</span><br>
|
||||||
|
response.staffer = { id, name, territory };<br>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="narr">
|
||||||
|
The corpus stays intact. The relevance gradient is per coordinator. As each accumulates fills, their slice of the playbook compounds independently. The architecture generalizes — every new metro adds territories, not code paths.
|
||||||
|
</div>
|
||||||
|
<div class="ref"><strong>Code:</strong> mcp-server/index.ts <code>STAFFERS</code> roster + <code>lookupStaffer()</code> · <code>/staffers</code> endpoint · <code>/intelligence/chat</code> smart_search route · <strong>UI:</strong> staffer dropdown in mcp-server/search.html</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chapter">
|
||||||
|
<div class="num">Chapter 6</div>
|
||||||
|
<h2>Construction Activity Signal Engine — the corpus is also a market signal</h2>
|
||||||
|
<div class="lede">Every contractor in this corpus is also a forward indicator on the public equities they touch. Permits filed today predict construction starts ~45 days out, staffing ~30, revenue recognition months later. The associated-ticker network surfaces this signal <em>before</em> any 10-Q. The architecture is metro-agnostic — Chicago is Phase 1; NYC DOB, LA County, Houston BCD, Boston ISD ship as Socrata-shaped adapters.</div>
|
||||||
|
|
||||||
|
<h3>Three flavors of attribution</h3>
|
||||||
|
<div class="math">
|
||||||
|
<span class="c">// per contractor in /intelligence/profiler_index:</span><br>
|
||||||
|
direct <span class="c">// contractor IS a public issuer → SEC tickers index match</span><br>
|
||||||
|
parent <span class="c">// curated KNOWN_PARENT_MAP — Turner → HOC.DE via Hochtief AG</span><br>
|
||||||
|
associated <span class="c">// co-permit network — Bob's Electric appears with TARGET CORPORATION</span><br>
|
||||||
|
<span class="c">// 3+ times → inherits TGT as an associated indicator</span>
|
||||||
|
</div>
|
||||||
|
<div class="narr">
|
||||||
|
The associated path is the moat. A staffing-permit dataset that maps contractor-to-public-issuer is not commercially available; we synthesize it from the Socrata co-occurrence graph. Every additional metro multiplies edges.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Building Activity Index (BAI)</h3>
|
||||||
|
<div class="math">
|
||||||
|
<span class="c">// BAI = attribution-weighted average day-change across surfaced issuers:</span><br>
|
||||||
|
BAI = Σ (day_change_pct × attribution_count) / Σ attribution_count<br>
|
||||||
|
<br>
|
||||||
|
<span class="c">// Indexed build value = total $ of permits attributable to ANY public issuer</span><br>
|
||||||
|
<span class="c">// Network depth = issuers / total attribution edges</span>
|
||||||
|
</div>
|
||||||
|
<div class="narr">
|
||||||
|
Run BAI daily, save the series, and you've got a backtestable thesis in months. Today's surface is Chicago-only with ~9 issuers; the curve scales linearly with metros added — and the marginal cost of a new metro is one Socrata adapter.
|
||||||
|
</div>
|
||||||
|
<div class="ref"><strong>Code:</strong> mcp-server/index.ts <code>/intelligence/profiler_index</code> + <code>/intelligence/ticker_quotes</code> · entity.ts <code>lookupTickerLite()</code> · <code>fetchStooqQuote()</code> · <strong>UI:</strong> /profiler · <strong>Data sources:</strong> SEC company_tickers.json (in-memory index) + Stooq CSV API + curated parent-link map</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chapter">
|
||||||
|
<div class="num">Chapter 7</div>
|
||||||
<h2>Key architectural choices — what was picked and why</h2>
|
<h2>Key architectural choices — what was picked and why</h2>
|
||||||
<div class="lede">Each choice is documented in <code>docs/DECISIONS.md</code> (Architecture Decision Records). If you dispute any of these, the ADR names the alternatives we rejected and the measurement that drove the call.</div>
|
<div class="lede">Each choice is documented in <code>docs/DECISIONS.md</code> (Architecture Decision Records). If you dispute any of these, the ADR names the alternatives we rejected and the measurement that drove the call.</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -314,62 +386,95 @@ pre{background:#161b22;border:1px solid #171d27;border-radius:8px;padding:14px 1
|
|||||||
<div class="row accent-r">
|
<div class="row accent-r">
|
||||||
<div style="flex:1"><div class="title">ADR-020 · Idempotent register() with schema-fingerprint gate</div><div class="meta">Same (name, fingerprint) reuses manifest. Different fingerprint = 409 Conflict. Prevents silent duplicate manifests. Cleanup run collapsed 374 → 31 datasets.</div></div>
|
<div style="flex:1"><div class="title">ADR-020 · Idempotent register() with schema-fingerprint gate</div><div class="meta">Same (name, fingerprint) reuses manifest. Different fingerprint = 409 Conflict. Prevents silent duplicate manifests. Cleanup run collapsed 374 → 31 datasets.</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row accent-r">
|
||||||
|
<div style="flex:1"><div class="title">ADR-021 · Semantic-correctness matrix layer</div><div class="meta">Pathway memory carries semantic flags (UnitMismatch, TypeConfusion, OffByOne, StaleReference, DeadCode, BoundaryViolation, …) on every trace. New reviews see prior bug fingerprints as a preamble; recurrent classes get caught on first read. Compounds across files in the same crate.</div></div>
|
||||||
|
</div>
|
||||||
<div class="row accent-l">
|
<div class="row accent-l">
|
||||||
<div style="flex:1"><div class="title">Phase 19 design note · Statistical + semantic, not neural</div><div class="meta">Meta-index is cosine similarity + endorsement aggregation. No model training. Rebuildable from <code>successful_playbooks</code> alone. Neural re-ranker deferred to Phase 20+ only if statistical floor plateaus.</div></div>
|
<div style="flex:1"><div class="title">Phase 19 design note · Statistical + semantic, not neural</div><div class="meta">Meta-index is cosine similarity + endorsement aggregation. No model training. Rebuildable from <code>successful_playbooks</code> alone. Neural re-ranker deferred to Phase 20+ only if statistical floor plateaus.</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row accent-l">
|
||||||
|
<div style="flex:1"><div class="title">Distillation freeze · v1.0.0 at e7636f2</div><div class="meta">145 tests · 22/22 acceptance · 16/16 audit-full · bit-identical reproducibility. Multi-layer contamination firewall on SFT exports. Substrate the auditor + mode runner sit on; "the system regressed" questions bisect against this anchor.</div></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chapter">
|
<div class="chapter">
|
||||||
<div class="num">Chapter 6</div>
|
<div class="num">Chapter 8</div>
|
||||||
<h2>Measured at scale, on this machine</h2>
|
<h2>Measured at scale, on this machine</h2>
|
||||||
<div class="lede">Hardware: i9 + 128GB RAM + Nvidia A4000 16GB VRAM. Numbers below are from <em>this</em> running instance. Refresh the page and they'll recompute.</div>
|
<div class="lede">Hardware: i9 + 128GB RAM + Nvidia A4000 16GB VRAM + 2.5GB symmetric. Numbers below are from <em>this</em> running instance. Refresh the page and they'll recompute.</div>
|
||||||
<div class="grid" id="ch6-scale"><div class="loading">Loading scale data…</div></div>
|
<div class="grid" id="ch6-scale"><div class="loading">Loading scale data…</div></div>
|
||||||
<div id="ch6-recall" style="margin-top:10px"></div>
|
<div id="ch6-recall" style="margin-top:10px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chapter">
|
<div class="chapter">
|
||||||
<div class="num">Chapter 7</div>
|
<div class="num">Chapter 9</div>
|
||||||
<h2>Verify or dispute — reproduce it yourself</h2>
|
<h2>Verify or dispute — reproduce it yourself</h2>
|
||||||
<div class="lede">Every claim below is a curl away from falsification.</div>
|
<div class="lede">Every claim above is a curl away from falsification.</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="narr"><strong>Health.</strong> Should return <code>lakehouse ok</code>.</div>
|
<div class="narr"><strong>Gateway health.</strong> Returns provider matrix + worker count.</div>
|
||||||
<pre>curl http://localhost:3100/health</pre>
|
<pre>curl -s http://localhost:3100/v1/health | jq</pre>
|
||||||
<div class="narr"><strong>Any SQL on multi-million-row Parquet.</strong> Sub-100ms typical.</div>
|
<div class="narr"><strong>Any SQL on multi-million-row Parquet.</strong> Sub-100ms typical.</div>
|
||||||
<pre>curl -s -X POST http://localhost:3100/query/sql \
|
<pre>curl -s -X POST http://localhost:3100/query/sql \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"sql":"SELECT role, COUNT(*) FROM workers_500k WHERE state=\"IL\" GROUP BY role LIMIT 5"}'</pre>
|
-d '{"sql":"SELECT role, COUNT(*) FROM workers_500k WHERE state=\"IL\" GROUP BY role LIMIT 5"}'</pre>
|
||||||
<div class="narr"><strong>Hybrid search with playbook boost.</strong> The whole Phase 19 feedback loop in one request.</div>
|
<div class="narr"><strong>Hybrid search with playbook boost.</strong> SQL filter + vector rerank + playbook memory in one call.</div>
|
||||||
<pre>curl -s -X POST http://localhost:3100/vectors/hybrid \
|
<pre>curl -s -X POST http://localhost:3100/vectors/hybrid \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"index_name":"workers_500k_v1",
|
-d '{"index_name":"workers_500k_v1",
|
||||||
"sql_filter":"role = '\''Forklift Operator'\'' AND city = '\''Chicago'\'' AND CAST(availability AS DOUBLE) > 0.5",
|
"sql_filter":"role = '\''Forklift Operator'\'' AND city = '\''Chicago'\'' AND CAST(availability AS DOUBLE) > 0.5",
|
||||||
"question":"reliable forklift operator",
|
"question":"reliable forklift operator",
|
||||||
"top_k":5,"use_playbook_memory":true,"playbook_memory_k":200}'</pre>
|
"top_k":5,"use_playbook_memory":true,"playbook_memory_k":200}'</pre>
|
||||||
<div class="narr"><strong>Playbook memory stats.</strong> Count + endorsed names + sample.</div>
|
<div class="narr"><strong>Pathway memory stats.</strong> System-level hot-swap signal — should show 88 traces / 11 replays / 100% reuse rate (probation gate crossed).</div>
|
||||||
<pre>curl http://localhost:3100/vectors/playbook_memory/stats</pre>
|
<pre>curl -s http://localhost:3100/vectors/pathway/stats | jq</pre>
|
||||||
<div class="narr"><strong>Pattern discovery.</strong> What do past similar fills have in common?</div>
|
<div class="narr"><strong>Per-staffer scoping.</strong> Same query, different rosters per coordinator.</div>
|
||||||
<pre>curl -s -X POST http://localhost:3100/vectors/playbook_memory/patterns \
|
<pre>for s in maria devon aisha; do
|
||||||
|
curl -s -X POST http://localhost:3700/intelligence/chat \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "{\"message\":\"forklift operators\",\"staffer_id\":\"$s\"}" \
|
||||||
|
| jq -r ".staffer.name + \": \" + (.sql_results | length | tostring) + \" workers, top: \" + (.sql_results[0].name + \" in \" + .sql_results[0].city + \", \" + .sql_results[0].state)"
|
||||||
|
done
|
||||||
|
# Maria: 167 workers, top: ... in Chicago, IL
|
||||||
|
# Devon: 89 workers, top: ... in Fort Wayne, IN
|
||||||
|
# Aisha: 16 workers, top: ... in Milwaukee, WI</pre>
|
||||||
|
<div class="narr"><strong>Late-worker triage in one shot.</strong> Pulls profile + 5 backfills + drafts SMS. Should respond in under 300ms.</div>
|
||||||
|
<pre>curl -s -X POST http://localhost:3700/intelligence/chat \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"query":"Forklift Operator in Chicago, IL","top_k_playbooks":25,"min_trait_frequency":0.3}'</pre>
|
-d '{"message":"Marcus running late site 4422"}' | jq</pre>
|
||||||
<div class="narr"><strong>Run the dual-agent scenario yourself.</strong> All 5 events, real fills, real artifacts.</div>
|
<div class="narr"><strong>Construction Activity Signal Engine.</strong> Profiler index with attribution, cost, last filed.</div>
|
||||||
|
<pre>curl -s -X POST http://localhost:3700/intelligence/profiler_index \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"limit":10}' \
|
||||||
|
| jq '.contractors[] | {name, permits, total_cost, direct: (.tickers.direct | map(.ticker)), associated: (.tickers.associated | map(.ticker + " ←via " + .partner_name))}'</pre>
|
||||||
|
<div class="narr"><strong>Live ticker quotes.</strong> Batch Stooq pull for the basket.</div>
|
||||||
|
<pre>curl -s -X POST http://localhost:3700/intelligence/ticker_quotes \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"tickers":["TGT","JPM","BALY","WBA","MCD"]}' | jq .quotes</pre>
|
||||||
|
<div class="narr"><strong>Audit trail — read any verdict on PR #11.</strong> Independent claim-vs-diff verifier output.</div>
|
||||||
|
<pre>ls /home/profit/lakehouse/data/_auditor/kimi_verdicts/
|
||||||
|
# 11-c3c9c2174a91.json 11-ca7375ea2b17.json 11-2d9cb128bf42.json …
|
||||||
|
jq '.findings[0:3]' /home/profit/lakehouse/data/_auditor/kimi_verdicts/11-c3c9c2174a91.json</pre>
|
||||||
|
<div class="narr"><strong>Distillation acceptance gate.</strong> 22/22 invariants must pass for any commit that touches the substrate.</div>
|
||||||
<pre>cd /home/profit/lakehouse
|
<pre>cd /home/profit/lakehouse
|
||||||
bun run tests/multi-agent/scenario.ts
|
bun test auditor/schemas/distillation/ tests/distillation/
|
||||||
# Output: tests/multi-agent/playbooks/scenario-<timestamp>/report.md</pre>
|
# Expect: 145 pass · 0 fail · 372 expect() calls</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chapter">
|
<div class="chapter">
|
||||||
<div class="num">Chapter 8</div>
|
<div class="num">Chapter 10</div>
|
||||||
<h2>What we are <em>not</em> claiming</h2>
|
<h2>What we are <em>not</em> claiming</h2>
|
||||||
<div class="lede">Every impressive-sounding number comes with a footnote. Here are the honest limits.</div>
|
<div class="lede">Every impressive-sounding number comes with a footnote. Here are the honest limits as of 2026-04-27.</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="row accent-a"><div style="flex:1"><div class="title">workers_500k is synthetic.</div><div class="meta">Real client ATS export replaces this table. Schema is deliberately identical to a production ATS.</div></div></div>
|
<div class="row accent-a"><div style="flex:1"><div class="title">workers_500k is synthetic.</div><div class="meta">Real client ATS export replaces this table. Schema is deliberately identical to a production ATS so the swap is config, not code.</div></div></div>
|
||||||
<div class="row accent-a"><div style="flex:1"><div class="title">candidates table has 1,000 rows.</div><div class="meta">Intentionally small for demo. call_log references higher candidate_ids that don't cross-reference — this is a dataset alignment issue, not a pipeline issue.</div></div></div>
|
<div class="row accent-a"><div style="flex:1"><div class="title">candidates table is light at 1,000 rows.</div><div class="meta">Intentionally small. Live PII-safe view layer is built; replacing the small table with a 100K+ ATS is a one-line config flip.</div></div></div>
|
||||||
<div class="row accent-b"><div style="flex:1"><div class="title">Chicago permit data is real.</div><div class="meta">Pulled live from data.cityofchicago.org/resource/ydr8-5enu.json (Socrata API). Not synthetic. Not cached.</div></div></div>
|
<div class="row accent-b"><div style="flex:1"><div class="title">Chicago permit data is real.</div><div class="meta">Pulled live from data.cityofchicago.org/resource/ydr8-5enu.json (Socrata). Not synthetic. Not cached. Verifiable address-by-address.</div></div></div>
|
||||||
<div class="row accent-l"><div style="flex:1"><div class="title">Playbook memory is seeded from demo runs.</div><div class="meta">The pipeline that seeds it is identical to what a live recruiter would trigger via /log. Same code path.</div></div></div>
|
<div class="row accent-l"><div style="flex:1"><div class="title">Playbook memory is seeded from demo runs.</div><div class="meta">Same code path that seeds in production: every /log from the recruiter UI triggers seed → persist_sql. Demo seeds use the same shape as live operations.</div></div></div>
|
||||||
<div class="row accent-w"><div style="flex:1"><div class="title">Local 7B models (mistral, qwen2.5) are imperfect.</div><div class="meta">They occasionally malform tool calls or drop fields. Multi-agent scenarios seal roughly 40-80% in one run. Larger models or constrained decoding would improve this. Not a substrate problem.</div></div></div>
|
<div class="row accent-l"><div style="flex:1"><div class="title">Pathway memory probation gate is crossed.</div><div class="meta">88 traces, 11 replays, 11 successful, 100% reuse rate. Any pathway that fails to clear ≥0.80 success_rate after ≥3 replays gets retired automatically (sticky flag prevents oscillation).</div></div></div>
|
||||||
|
<div class="row accent-w"><div style="flex:1"><div class="title">SEC name-to-ticker fuzzy matcher has rare false positives.</div><div class="meta">For names with no clean SEC match the matcher occasionally surfaces a same-keyword small-cap (saw FLG attach to a PNC-adjacent contractor once). Kept conservative — minimum 2 non-stopword overlap. Tightenable to require explicit allow-list for production trading use.</div></div></div>
|
||||||
|
<div class="row accent-r"><div style="flex:1"><div class="title">12 awaiting public-data sources are placeholders.</div><div class="meta">DOL Wage & Hour, EPA ECHO, MSHA, BBB, PACER, UCC liens, D&B, etc. — listed by name on every contractor profile with a one-line "would show:" sample. Not yet wired. Each ships as a Socrata-style adapter; engineering scope is concrete.</div></div></div>
|
||||||
<div class="row accent-r"><div style="flex:1"><div class="title">No rate/margin awareness yet.</div><div class="meta">Worker pay expectations vs contract bill rates are not modeled. Flagged as a Phase 20 item; no architectural blocker.</div></div></div>
|
<div class="row accent-r"><div style="flex:1"><div class="title">No rate/margin awareness yet.</div><div class="meta">Worker pay expectations vs contract bill rates are not modeled. Flagged as a Phase 20 item; no architectural blocker.</div></div></div>
|
||||||
|
<div class="row accent-r"><div style="flex:1"><div class="title">BAI is a thesis, not a backtested signal.</div><div class="meta">The Building Activity Index is computed live from current attribution + day-change. To have a backtestable thesis we need the daily series saved over months. Architectural support is there (data/_kb/audit_baselines.jsonl pattern); just hasn't been running long enough.</div></div></div>
|
||||||
|
<div class="row accent-r"><div style="flex:1"><div class="title">Single-metro today.</div><div class="meta">Chicago via Socrata. NYC DOB, LA County, Houston BCD, Boston ISD, DC DCRA all use Socrata-equivalent APIs — adapters are config-only. Each new metro multiplies the network without multiplying the codebase.</div></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -394,8 +499,72 @@ function apiPost(path, body){
|
|||||||
|
|
||||||
window.addEventListener('load',function(){
|
window.addEventListener('load',function(){
|
||||||
loadLiveSections();
|
loadLiveSections();
|
||||||
|
loadPathwayLive();
|
||||||
|
loadSignalLive();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Pathway memory live counters in Chapter 4 — small inline spans.
|
||||||
|
function loadPathwayLive(){
|
||||||
|
fetch(A+'/api/vectors/pathway/stats').then(function(r){return r.json()}).then(function(p){
|
||||||
|
if(!p) return;
|
||||||
|
var t=document.getElementById('pwm-traces');
|
||||||
|
var r=document.getElementById('pwm-replays');
|
||||||
|
var rate=document.getElementById('pwm-rate');
|
||||||
|
if(t) t.textContent = (p.total_pathways||0) + ' traces';
|
||||||
|
if(r) r.textContent = (p.successful_replays||0) + '/' + (p.total_replays||0);
|
||||||
|
if(rate) rate.textContent = Math.round((p.replay_success_rate||0)*100) + '%';
|
||||||
|
}).catch(function(){});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live tile under Chapter 1 — what the signal engine sees in this view.
|
||||||
|
function loadSignalLive(){
|
||||||
|
apiPost('/intelligence/profiler_index',{limit:200}).then(function(d){
|
||||||
|
var host=document.getElementById('ch1-live');if(!host) return;
|
||||||
|
host.textContent='';
|
||||||
|
var rows=d.contractors||[];
|
||||||
|
if(!rows.length) return;
|
||||||
|
// Aggregate basket
|
||||||
|
var byTk={};
|
||||||
|
rows.forEach(function(r){
|
||||||
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||||||
|
ts.forEach(function(t){
|
||||||
|
if(!t||!t.ticker) return;
|
||||||
|
if(!byTk[t.ticker]) byTk[t.ticker]={kinds:[],count:0};
|
||||||
|
byTk[t.ticker].count++;
|
||||||
|
if(byTk[t.ticker].kinds.indexOf(t.via)<0) byTk[t.ticker].kinds.push(t.via);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var basket=Object.values(byTk);
|
||||||
|
var attribCost=rows.reduce(function(s,r){
|
||||||
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||||||
|
return s + (ts.length>0 ? (r.total_cost||0) : 0);
|
||||||
|
},0);
|
||||||
|
if(!basket.length) return;
|
||||||
|
var card=el('div','card accent-l');
|
||||||
|
var hdr=el('div',null,'LIVE — Construction Activity Signal Engine');
|
||||||
|
hdr.style.cssText='font-size:10px;color:#3fb950;text-transform:uppercase;letter-spacing:1.4px;font-weight:700;margin-bottom:8px';
|
||||||
|
card.appendChild(hdr);
|
||||||
|
var line=document.createElement('div');
|
||||||
|
line.style.cssText='display:flex;gap:24px;flex-wrap:wrap;font-size:13px';
|
||||||
|
function block(num,lab){
|
||||||
|
var b=document.createElement('div');
|
||||||
|
var n=document.createElement('div');n.style.cssText='font-size:18px;font-weight:700;color:#e6edf3;font-family:ui-monospace,monospace';n.textContent=num;
|
||||||
|
var l=document.createElement('div');l.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.2px;font-weight:600';l.textContent=lab;
|
||||||
|
b.appendChild(n);b.appendChild(l);return b;
|
||||||
|
}
|
||||||
|
var bav = attribCost>=1e9?'$'+(attribCost/1e9).toFixed(2)+'B':attribCost>=1e6?'$'+(attribCost/1e6).toFixed(0)+'M':'$'+Math.round(attribCost/1e3)+'K';
|
||||||
|
line.appendChild(block(basket.length+'', 'Public issuers in scope'));
|
||||||
|
line.appendChild(block(bav, 'Attributed build value'));
|
||||||
|
line.appendChild(block(rows.length+'', 'Contractors indexed'));
|
||||||
|
line.appendChild(block(basket.reduce(function(s,b){return s+b.count},0)+'', 'Attribution edges'));
|
||||||
|
card.appendChild(line);
|
||||||
|
var note=el('div',null,'Computed live from /intelligence/profiler_index in '+(d.duration_ms||0)+'ms · click any of the chapter-9 curl lines to verify');
|
||||||
|
note.style.cssText='font-size:11px;color:#545d68;margin-top:10px;font-family:ui-monospace,monospace';
|
||||||
|
card.appendChild(note);
|
||||||
|
host.appendChild(card);
|
||||||
|
}).catch(function(){});
|
||||||
|
}
|
||||||
|
|
||||||
function loadLiveSections(){
|
function loadLiveSections(){
|
||||||
apiPost('/proof.json',{}).then(function(r){
|
apiPost('/proof.json',{}).then(function(r){
|
||||||
var host1=document.getElementById('ch1-tests');host1.textContent='';
|
var host1=document.getElementById('ch1-tests');host1.textContent='';
|
||||||
|
|||||||
@ -48,7 +48,19 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
|
|||||||
/* Workers */
|
/* Workers */
|
||||||
.iworker{display:flex;align-items:center;gap:12px;padding:10px 12px;background:#161b22;border-radius:8px;margin-bottom:4px;transition:background 0.15s}
|
.iworker{display:flex;align-items:center;gap:12px;padding:10px 12px;background:#161b22;border-radius:8px;margin-bottom:4px;transition:background 0.15s}
|
||||||
.iworker:hover{background:#1c2333}
|
.iworker:hover{background:#1c2333}
|
||||||
.iworker .av{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:12px;color:#e6edf3;flex-shrink:0}
|
.iworker .av{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:13px;color:#c9d1d9;flex-shrink:0;background:#161b22;border:1px solid #21262d;letter-spacing:0.5px;font-family:'Inter',-apple-system,sans-serif;overflow:hidden;position:relative}
|
||||||
|
.iworker .av img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
|
||||||
|
.iworker .role-pill{display:inline-block;font-size:10px;padding:2px 8px;border-radius:3px;background:#0d1117;color:#8b949e;margin-right:8px;font-weight:600;letter-spacing:0.4px;text-transform:uppercase;border-left:2px solid #30363d}
|
||||||
|
.iworker[data-role-band="warehouse"]{border-left:3px solid #58a6ff}
|
||||||
|
.iworker[data-role-band="production"]{border-left:3px solid #d29922}
|
||||||
|
.iworker[data-role-band="trades"]{border-left:3px solid #bc8cff}
|
||||||
|
.iworker[data-role-band="driver"]{border-left:3px solid #3fb950}
|
||||||
|
.iworker[data-role-band="lead"]{border-left:3px solid #f0883e}
|
||||||
|
.iworker .role-pill[data-rb="warehouse"]{border-left-color:#58a6ff;color:#79c0ff}
|
||||||
|
.iworker .role-pill[data-rb="production"]{border-left-color:#d29922;color:#e3b341}
|
||||||
|
.iworker .role-pill[data-rb="trades"]{border-left-color:#bc8cff;color:#d2a8ff}
|
||||||
|
.iworker .role-pill[data-rb="driver"]{border-left-color:#3fb950;color:#56d364}
|
||||||
|
.iworker .role-pill[data-rb="lead"]{border-left-color:#f0883e;color:#ffa657}
|
||||||
.iworker .info{flex:1;min-width:0}
|
.iworker .info{flex:1;min-width:0}
|
||||||
.iworker .nm{font-weight:600;color:#e6edf3;font-size:13px}
|
.iworker .nm{font-weight:600;color:#e6edf3;font-size:13px}
|
||||||
.iworker .detail{color:#545d68;font-size:11px}
|
.iworker .detail{color:#545d68;font-size:11px}
|
||||||
@ -202,6 +214,7 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
|
|||||||
<nav>
|
<nav>
|
||||||
<a href="." class="active">Dashboard</a>
|
<a href="." class="active">Dashboard</a>
|
||||||
<a href="console">Walkthrough</a>
|
<a href="console">Walkthrough</a>
|
||||||
|
<a href="profiler">Profiler</a>
|
||||||
<a href="proof">Architecture</a>
|
<a href="proof">Architecture</a>
|
||||||
<a href="spec">Spec</a>
|
<a href="spec">Spec</a>
|
||||||
<a href="onboard">Onboard</a>
|
<a href="onboard">Onboard</a>
|
||||||
@ -302,26 +315,33 @@ body{font-family:'Inter',-apple-system,system-ui,'Segoe UI',sans-serif;backgroun
|
|||||||
</p>
|
</p>
|
||||||
<div id="ws-samples" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;font-size:11px"></div>
|
<div id="ws-samples" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;font-size:11px"></div>
|
||||||
<input type="text" id="sq" placeholder="Try: reliable forklift operator available in Nashville" onkeydown="if(event.key==='Enter')doSearch()" style="width:100%;box-sizing:border-box;padding:10px 12px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;margin-bottom:8px">
|
<input type="text" id="sq" placeholder="Try: reliable forklift operator available in Nashville" onkeydown="if(event.key==='Enter')doSearch()" style="width:100%;box-sizing:border-box;padding:10px 12px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;font-size:13px;margin-bottom:8px">
|
||||||
<div class="srow" style="display:flex;gap:8px;margin-bottom:8px">
|
<div class="srow" style="display:flex;gap:8px;margin-bottom:8px;align-items:center">
|
||||||
|
<!-- Per-staffer hot-swap selector. When a staffer is chosen, every
|
||||||
|
search and triage scopes to their territory; the playbook MEMORY
|
||||||
|
panel labels itself "Maria's recent fills" instead of generic. -->
|
||||||
|
<select id="sstaffer" style="flex:0 0 auto;padding:8px 10px;background:#0d1117;border:1px solid #58a6ff66;border-radius:6px;color:#e6edf3;font-weight:600;cursor:pointer" title="Act as this coordinator — playbook context will scope to their territory">
|
||||||
|
<option value="">All staffers</option>
|
||||||
|
</select>
|
||||||
<select id="sst" style="flex:1;padding:8px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3"><option value="">Any State</option></select>
|
<select id="sst" style="flex:1;padding:8px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3"><option value="">Any State</option></select>
|
||||||
<select id="srl" style="flex:1;padding:8px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3"><option value="">Any Role</option></select>
|
<select id="srl" style="flex:1;padding:8px;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3"><option value="">Any Role</option></select>
|
||||||
<button class="sbtn" onclick="doSearch()" style="padding:8px 16px;background:#238636;border:none;border-radius:6px;color:#fff;font-weight:600;cursor:pointer">Find Workers</button>
|
<button class="sbtn" onclick="doSearch()" style="padding:8px 16px;background:#238636;border:none;border-radius:6px;color:#fff;font-weight:600;cursor:pointer">Find Workers</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="sstaffer-greeting" style="font-size:11px;color:#58a6ff;margin-bottom:8px;min-height:14px"></div>
|
||||||
<div id="sresults"></div>
|
<div id="sresults"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ═══ ④ System Activity — how the substrate is learning ═══ -->
|
<!-- ═══ ④ System Activity — what the substrate has learned to do ═══ -->
|
||||||
<div class="section" id="learning-section">
|
<div class="section" id="learning-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<span class="section-title" style="color:#e6edf3;font-size:13px">④ System Activity — how the substrate learns</span>
|
<span class="section-title" style="color:#e6edf3;font-size:13px">④ System Activity — what the substrate has learned to do</span>
|
||||||
<span class="section-meta">Playbook memory, pathway traces, self-tuning indices</span>
|
<span class="section-meta">Live capabilities · pulled from each subsystem on load</span>
|
||||||
</div>
|
</div>
|
||||||
<p style="color:#8b949e;font-size:11px;margin:0 0 12px;line-height:1.5;max-width:680px">
|
<p style="color:#8b949e;font-size:11px;margin:0 0 12px;line-height:1.5;max-width:780px">
|
||||||
Every completed fill, every accepted playbook, every rejected candidate feeds
|
Each tile is a capability the system has acquired and the live metric proving
|
||||||
back into the substrate. This strip shows what the system has learned since
|
it's running. Operational learning (fills, playbooks, hot-swaps) compounds
|
||||||
the last run — which patterns are compounding, which memories are fresh,
|
inside each capability; what changes here is the <em>set of things the
|
||||||
which indices are being exercised. If it's empty, the system hasn't seen
|
substrate knows how to do</em>. The architecture is metro-agnostic — every
|
||||||
enough traffic yet to form a memory worth showing.
|
capability replicates by config, not code.
|
||||||
</p>
|
</p>
|
||||||
<div id="learning"></div>
|
<div id="learning"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -363,7 +383,38 @@ var P=location.pathname.indexOf('/lakehouse')>=0?'/lakehouse':'';
|
|||||||
var A=location.origin+P;
|
var A=location.origin+P;
|
||||||
var AC=['#1a2744','#1a3a2a','#2a1a3a','#3a2a1a','#1a3a3a','#2a2a1a'];
|
var AC=['#1a2744','#1a3a2a','#2a1a3a','#3a2a1a','#1a3a3a','#2a2a1a'];
|
||||||
var lastQuery='';
|
var lastQuery='';
|
||||||
window.addEventListener('load',function(){loadSystemSummary();loadLegacyBridge();loadDay();loadStaffingForecast();loadLiveContracts();loadMarket();loadLearning();loadWorkerSearchSamples();loadArchSignals()});
|
window.addEventListener('load',function(){loadSystemSummary();loadLegacyBridge();loadDay();loadStaffingForecast();loadLiveContracts();loadMarket();loadLearning();loadWorkerSearchSamples();loadArchSignals();loadStaffers()});
|
||||||
|
|
||||||
|
// Per-staffer hot-swap dropdown — runs independently of the simulation
|
||||||
|
// fetch so the staffer selector populates even if any other init step
|
||||||
|
// errors out. /api/staffers returns the synthetic coordinator roster.
|
||||||
|
function loadStaffers(){
|
||||||
|
var sel=document.getElementById('sstaffer');
|
||||||
|
var greeting=document.getElementById('sstaffer-greeting');
|
||||||
|
if(!sel) return;
|
||||||
|
// /staffers (not /api/staffers) — the /api/* generic passthrough
|
||||||
|
// forwards anything under /api/ to the Rust gateway on :3100 and the
|
||||||
|
// gateway doesn't know the staffer roster (it lives in the mcp-server
|
||||||
|
// module). The bare /staffers route serves directly.
|
||||||
|
fetch(A+'/staffers').then(function(r){return r.json()}).then(function(d){
|
||||||
|
(d.staffers||[]).forEach(function(s){
|
||||||
|
var o=document.createElement('option');o.value=s.id;o.textContent=s.display||s.name;
|
||||||
|
sel.appendChild(o);
|
||||||
|
});
|
||||||
|
sel._roster=d.staffers||[];
|
||||||
|
}).catch(function(){});
|
||||||
|
sel.addEventListener('change',function(){
|
||||||
|
var roster=sel._roster||[];
|
||||||
|
var s=roster.find(function(x){return x.id===sel.value});
|
||||||
|
if(s){
|
||||||
|
greeting.textContent='Acting as '+s.name+' — '+(s.greeting||'')+' · territory: '+s.territory.cities.slice(0,3).join(', ')+'…';
|
||||||
|
var stSel=document.getElementById('sst');
|
||||||
|
if(stSel && !stSel.value){stSel.value=s.territory.state}
|
||||||
|
}else{
|
||||||
|
greeting.textContent='';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Deep-link: visiting the dashboard with #open-briefs in the URL auto-
|
// Deep-link: visiting the dashboard with #open-briefs in the URL auto-
|
||||||
// expands every Entity Brief panel once the contract cards finish
|
// expands every Entity Brief panel once the contract cards finish
|
||||||
@ -1152,7 +1203,7 @@ function renderEntityBrief(host, brief){
|
|||||||
var tkr=document.createElement('span');
|
var tkr=document.createElement('span');
|
||||||
tkr.style.cssText='font-family:ui-monospace,SFMono-Regular,monospace;background:#161b22;padding:2px 6px;border-radius:4px;color:#58a6ff;border:1px solid #58a6ff44;font-weight:600;font-size:10px';
|
tkr.style.cssText='font-family:ui-monospace,SFMono-Regular,monospace;background:#161b22;padding:2px 6px;border-radius:4px;color:#58a6ff;border:1px solid #58a6ff44;font-weight:600;font-size:10px';
|
||||||
tkr.textContent=e.ticker||'LLC·?';
|
tkr.textContent=e.ticker||'LLC·?';
|
||||||
var nm=document.createElement('a');nm.href='/contractor?name='+encodeURIComponent(e.display_name||'');
|
var nm=document.createElement('a');nm.href=P+'/contractor?name='+encodeURIComponent(e.display_name||'');
|
||||||
nm.target='_blank';nm.rel='noopener';
|
nm.target='_blank';nm.rel='noopener';
|
||||||
nm.style.cssText='color:#e6edf3;font-weight:600;font-size:12px;text-decoration:none;border-bottom:1px dotted #58a6ff44';
|
nm.style.cssText='color:#e6edf3;font-weight:600;font-size:12px;text-decoration:none;border-bottom:1px dotted #58a6ff44';
|
||||||
nm.title='Open full contractor profile';
|
nm.title='Open full contractor profile';
|
||||||
@ -1609,10 +1660,29 @@ function loadLiveContracts(){
|
|||||||
var ebLabel=document.createElement('span');ebLabel.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1px';
|
var ebLabel=document.createElement('span');ebLabel.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1px';
|
||||||
ebLabel.textContent='PROJECT INDEX — Build Signals';
|
ebLabel.textContent='PROJECT INDEX — Build Signals';
|
||||||
var ebTags=document.createElement('span');ebTags.style.cssText='color:#e6edf3;font-size:11px;flex:1;font-weight:500';
|
var ebTags=document.createElement('span');ebTags.style.cssText='color:#e6edf3;font-size:11px;flex:1;font-weight:500';
|
||||||
|
// Contractor names link to the full profile page. Without anchors,
|
||||||
|
// clicking the preview did nothing — the only working contractor
|
||||||
|
// link was inside the lazy-loaded entity brief, which a coordinator
|
||||||
|
// wouldn't reach without first expanding the details.
|
||||||
var preview=[];
|
var preview=[];
|
||||||
|
function contactLink(n){
|
||||||
|
var a=document.createElement('a');
|
||||||
|
a.href=P+'/contractor?name='+encodeURIComponent(n);
|
||||||
|
a.target='_blank';a.rel='noopener';
|
||||||
|
a.style.cssText='color:inherit;text-decoration:none;border-bottom:1px dotted #58a6ff44';
|
||||||
|
a.title='Open full contractor profile';
|
||||||
|
a.textContent=n;
|
||||||
|
a.addEventListener('click',function(e){e.stopPropagation()}); // don't toggle the details
|
||||||
|
return a;
|
||||||
|
}
|
||||||
if(p.contact_1_name) preview.push(p.contact_1_name);
|
if(p.contact_1_name) preview.push(p.contact_1_name);
|
||||||
if(p.contact_2_name && p.contact_2_name!==p.contact_1_name) preview.push(p.contact_2_name);
|
if(p.contact_2_name && p.contact_2_name!==p.contact_1_name) preview.push(p.contact_2_name);
|
||||||
ebTags.textContent=preview.join(' · ');
|
if(preview.length){
|
||||||
|
preview.forEach(function(n,i){
|
||||||
|
if(i>0) ebTags.appendChild(document.createTextNode(' · '));
|
||||||
|
ebTags.appendChild(contactLink(n));
|
||||||
|
});
|
||||||
|
}
|
||||||
var ebMeta=document.createElement('span');ebMeta.style.cssText='color:#545d68;font-size:10px';
|
var ebMeta=document.createElement('span');ebMeta.style.cssText='color:#545d68;font-size:10px';
|
||||||
ebMeta.textContent='click → fetch OSHA + ILSOS';
|
ebMeta.textContent='click → fetch OSHA + ILSOS';
|
||||||
ebSum.appendChild(ebCaret);ebSum.appendChild(ebLabel);ebSum.appendChild(ebTags);ebSum.appendChild(ebMeta);
|
ebSum.appendChild(ebCaret);ebSum.appendChild(ebLabel);ebSum.appendChild(ebTags);ebSum.appendChild(ebMeta);
|
||||||
@ -2191,6 +2261,114 @@ function addBigMeter(parent,label,val,desc){
|
|||||||
d.appendChild(lb);d.appendChild(row);d.appendChild(ds);parent.appendChild(d);
|
d.appendChild(lb);d.appendChild(row);d.appendChild(ds);parent.appendChild(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First-name → gender hint for face-pool selection. Built from the
|
||||||
|
// most common 200 US given names. Lookup is best-effort — undefined
|
||||||
|
// returns means "no hint, draw from full pool." Don't surface this
|
||||||
|
// anywhere user-visible; it's purely a face-pool selector.
|
||||||
|
var FEMALE_NAMES = new Set(['Mary','Patricia','Jennifer','Linda','Elizabeth','Barbara','Susan','Jessica','Sarah','Karen','Lisa','Nancy','Betty','Sandra','Margaret','Ashley','Kimberly','Emily','Donna','Michelle','Carol','Amanda','Melissa','Deborah','Stephanie','Dorothy','Rebecca','Sharon','Laura','Cynthia','Amy','Kathleen','Angela','Shirley','Brenda','Emma','Anna','Pamela','Nicole','Samantha','Katherine','Christine','Helen','Debra','Rachel','Carolyn','Janet','Maria','Catherine','Heather','Diane','Olivia','Julie','Joyce','Victoria','Ruth','Virginia','Lauren','Kelly','Christina','Joan','Evelyn','Judith','Andrea','Hannah','Megan','Cheryl','Jacqueline','Martha','Madison','Teresa','Gloria','Sara','Janice','Ann','Kathryn','Abigail','Sophia','Frances','Jean','Alice','Judy','Isabella','Julia','Grace','Amber','Denise','Danielle','Marilyn','Beverly','Charlotte','Natalie','Theresa','Diana','Brittany','Doris','Kayla','Alexis','Lori','Marie','Carmen','Aisha','Rosa','Kim','Mia','Audrey','Erin','Tina','Vanessa','Tara','Wendy','Tanya','Maya','Crystal','Yvonne','Kara','Shannon','Brianna','Faith','Caroline','Carla','Tracey','Tracy','Rita','Dawn','Tiffany','Stacy','Stacey','Gina','Bonnie','Tammy','Joanne','Jamie','Tonya','Alyssa','Ariana','Elena','Ellie','Erica','Erika','Felicia','Holly','Jenna','Jenny','Krista','Kristen','Kristin','Krystal','Lana','Leah','Lucy','Mallory','Melinda','Meredith','Misty','Monica','Mya','Naomi','Paige','Patrice','Paula','Renee','Rhonda','Robin','Roxanne','Sadie','Selena','Shari','Shauna','Sierra','Skylar','Sonia','Stella','Tamara','Taryn','Trina','Veronica','Vivian','Whitney','Yolanda','Zoe','Lakeisha','Latoya','Tasha',
|
||||||
|
// Hispanic female
|
||||||
|
'Esperanza','Luz','Lucia','Camila','Valentina','Mariana','Catalina','Cristina','Daniela','Gabriela','Ximena','Adriana','Beatriz','Pilar','Consuelo','Dolores','Mercedes','Marisol','Guadalupe','Lupita','Inez','Itzel','Yesenia','Monserrat','Renata','Alejandra','Alma','Belen','Blanca','Esmeralda','Imelda','Lourdes','Magdalena','Olga','Refugio','Rocio','Susana','Anita','Fatima',
|
||||||
|
// South Asian female
|
||||||
|
'Priya','Anjali','Neha','Kavya','Pooja','Divya','Meera','Lakshmi','Rani','Asha','Saanvi','Aanya','Aaradhya','Shreya','Riya','Tanvi','Ishita','Shivani','Padma','Sita','Geeta','Rekha','Amira',
|
||||||
|
// East Asian female
|
||||||
|
'Mei','Sakura','Aiko','Sora','Chiyo','Hana','Eun','Xiu','Lan','Hua','Min','Xin','Ying','Zhen','Yan',
|
||||||
|
// Black female (additional)
|
||||||
|
'Imani','Keisha','Lakisha','Kenya','Tamika','Latanya','Latrice','Aaliyah','Kiara','Janelle','Jasmine','Tanisha','Maliyah','Imari','Nia','Zuri','Talia','Jada','Ebony','Dominique',
|
||||||
|
// Middle Eastern female
|
||||||
|
'Layla','Yasmin','Yara','Nadia','Zainab','Rania','Samira','Mariam','Salma','Dunia','Iman','Lina','Mona','Noor','Rana','Sabrina','Soha','Zara'
|
||||||
|
]);
|
||||||
|
var MALE_NAMES = new Set(['James','Robert','John','Michael','David','William','Richard','Joseph','Thomas','Charles','Christopher','Daniel','Matthew','Anthony','Mark','Donald','Steven','Paul','Andrew','Joshua','Kenneth','Kevin','Brian','George','Edward','Ronald','Timothy','Jason','Jeffrey','Ryan','Jacob','Gary','Nicholas','Eric','Jonathan','Stephen','Larry','Justin','Scott','Brandon','Benjamin','Samuel','Gregory','Frank','Alexander','Raymond','Patrick','Jack','Dennis','Jerry','Tyler','Aaron','Jose','Adam','Henry','Nathan','Douglas','Zachary','Peter','Kyle','Walter','Ethan','Jeremy','Harold','Keith','Christian','Roger','Noah','Gerald','Carl','Terry','Sean','Austin','Arthur','Lawrence','Jesse','Dylan','Bryan','Joe','Jordan','Billy','Bruce','Albert','Willie','Gabriel','Logan','Alan','Juan','Wayne','Roy','Ralph','Randy','Eugene','Vincent','Russell','Elijah','Louis','Bobby','Philip','Johnny','Marcus','Antonio','Carlos','Diego','Hector','Jorge','Julio','Manuel','Miguel','Pedro','Raul','Ricardo','Roberto','Sergio','Victor','Jamal','Xavier','DeShawn','Dwayne','Jermaine','Malik','Tyrone','Devon','Andre','Anwar','Brent','Calvin','Casey','Cody','Cole','Cory','Curt','Dale','Damon','Darius','Darrell','Dean','Derek','Donnie','Drew','Earl','Eddie','Floyd','Glenn','Greg','Howard','Ivan','Jared','Jay','Jeff','Joel','Johnnie','Lance','Lee','Leonard','Lloyd','Mario','Martin','Mason','Maurice','Max','Mitchell','Morgan','Nick','Norman','Oliver','Owen','Pete','Quincy','Rafael','Reggie','Rex','Ricky','Rod','Russ','Salvatore','Shane','Shaun','Stanley','Steve','Theodore','Todd','Travis','Trevor','Troy','Wade','Warren','Wesley',
|
||||||
|
// Hispanic male
|
||||||
|
'Alejandro','Andres','Mateo','Santiago','Sebastian','Emilio','Tomas','Joaquin','Ignacio','Salvador','Cesar','Arturo','Armando','Hugo','Marco','Felipe','Gerardo','Jaime','Leonardo','Luis','Pablo','Ramon','Reynaldo','Vincente','Javier','Esteban','Eduardo','Fernando','Humberto','Ernestino','Cristian','Hernan',
|
||||||
|
// South Asian male
|
||||||
|
'Raj','Anil','Rohan','Vikram','Arjun','Sanjay','Ravi','Krishna','Pradeep','Sunil','Amit','Deepak','Ashok','Manoj','Rahul','Vijay','Suresh','Naveen','Anand','Nikhil','Aditya','Karan','Rajesh','Ramesh','Kishore','Mohan','Ajay','Aarav','Ishaan',
|
||||||
|
// East Asian male
|
||||||
|
'Wei','Yi','Jin','Hiroshi','Akira','Kenji','Haruto','Hyun','Yoon','Kai','Long','Hong','Hao','Tao','Bao','Cheng','Feng','Qiang','Jian','Dong','Bin','Lei','Hui','Yu','Yuan',
|
||||||
|
// Black male (additional)
|
||||||
|
'Demetrius','Tyrese','Trevon','Kareem','DaQuan','Tyrell','Kwame','Khalil','Rashid','Terrell','Chauncey','Cedric','Imari','Jalen','Jaylen',
|
||||||
|
// Middle Eastern male
|
||||||
|
'Omar','Khalid','Hassan','Hussein','Ahmed','Mohamed','Mohammed','Ali','Karim','Yusuf','Ibrahim','Mahmoud','Saif','Bilal','Faisal','Hamza','Imran','Sami','Wael','Yasin','Zaid'
|
||||||
|
]);
|
||||||
|
function guessGenderFromFirstName(name){
|
||||||
|
if(!name) return null;
|
||||||
|
var clean = name.replace(/[^A-Za-z]/g,'');
|
||||||
|
if(!clean) return null;
|
||||||
|
// Title-case
|
||||||
|
var c = clean[0].toUpperCase() + clean.slice(1).toLowerCase();
|
||||||
|
if(FEMALE_NAMES.has(c)) return 'woman';
|
||||||
|
if(MALE_NAMES.has(c)) return 'man';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First-name → ethnicity hint. The synthetic-data generator built
|
||||||
|
// workers_500k from a multi-cultural name pool (Raj, DeShawn, Jamal,
|
||||||
|
// Mei, Wei, Carmen, Esperanza, Aisha, etc.). The hint is used ONLY
|
||||||
|
// to bias face-pool selection toward visually-aligned StyleGAN faces;
|
||||||
|
// it never surfaces in any worker-facing label or report. Buckets
|
||||||
|
// match common deepface "race" categories so the pool tags align
|
||||||
|
// when deepface is later run over the headshot pool.
|
||||||
|
var NAMES_SOUTH_ASIAN = new Set(['Raj','Anil','Rohan','Vikram','Arjun','Sanjay','Ravi','Krishna','Pradeep','Sunil','Amit','Deepak','Ashok','Manoj','Rahul','Vijay','Suresh','Naveen','Anand','Nikhil','Aditya','Karan','Rajesh','Ramesh','Kishore','Mohan','Ajay','Priya','Anjali','Neha','Kavya','Pooja','Divya','Meera','Lakshmi','Rani','Asha','Saanvi','Aanya','Aaradhya','Shreya','Riya','Tanvi','Ishita','Aarav','Ishaan','Shivani','Padma','Sita','Geeta','Rekha']);
|
||||||
|
var NAMES_EAST_ASIAN = new Set(['Wei','Mei','Yi','Jin','Chen','Lin','Liu','Wang','Zhang','Yang','Wu','Zhao','Sun','Hiroshi','Yuki','Akira','Kenji','Sakura','Aiko','Haruto','Sora','Chiyo','Hana','Hyun','Eun','Yoon','Kai','Long','Hong','Xiu','Lan','Hua','Hao','Tao','Bao','Cheng','Feng','Qiang','Jian','Dong','Bin','Min','Lei','Hui','Yu','Xin','Ying','Zhen','Yuan','Yan']);
|
||||||
|
var NAMES_HISPANIC = new Set(['Carmen','Carlos','Maria','Diego','Hector','Jorge','Julio','Manuel','Miguel','Pedro','Raul','Ricardo','Roberto','Sergio','Antonio','Esperanza','Luz','Sofia','Lucia','Isabella','Camila','Valentina','Mariana','Elena','Rosa','Catalina','Esteban','Fernando','Eduardo','Javier','Alejandro','Andres','Mateo','Santiago','Sebastian','Emilio','Tomas','Cristina','Daniela','Gabriela','Ximena','Adriana','Beatriz','Pilar','Consuelo','Dolores','Mercedes','Xavier','Marisol','Guadalupe','Lupita','Inez','Itzel','Yolanda','Yesenia','Monserrat','Renata','Ximena','Joaquin','Ignacio','Rafael','Salvador','Cesar','Arturo','Armando','Hugo','Marco','Alejandra','Alma','Belen','Blanca','Esmeralda','Fatima','Gloria','Imelda','Lourdes','Magdalena','Olga','Paula','Refugio','Rocio','Susana','Teresa','Veronica','Anita','Ernestino','Felipe','Gerardo','Humberto','Jaime','Leonardo','Luis','Pablo','Ramon','Reynaldo','Vincente']);
|
||||||
|
var NAMES_BLACK = new Set(['DeShawn','Jamal','Aisha','Latoya','Tyrone','Malik','Imani','Keisha','Tariq','Lakisha','Kenya','Tamika','Shaquille','Andre','Marcus','Demetrius','Jermaine','Reggie','Tyrese','Darius','Trevon','Kareem','Damon','Jalen','Jaylen','Dwayne','DaQuan','Latanya','Latrice','Aaliyah','Kiara','Janelle','Jasmine','Tanisha','Yolanda','Maurice','Tyrell','Kwame','Khalil','Rashid','Terrell','Chauncey','Cedric','Maliyah','Imari','Nia','Zuri','Talia','Jada','Ebony','Dominique']);
|
||||||
|
var NAMES_MIDDLE_EASTERN = new Set(['Layla','Omar','Khalid','Fatima','Yasmin','Hassan','Hussein','Ahmed','Mohamed','Mohammed','Ali','Karim','Yusuf','Yara','Nadia','Zainab','Rania','Samira','Mariam','Salma','Ibrahim','Mahmoud','Saif','Anwar','Bilal','Faisal','Hamza','Imran','Jamal','Rashid','Sami','Tariq','Wael','Yasin','Zaid','Amira','Dunia','Iman','Lina','Mona','Noor','Rana','Sabrina','Soha','Yara','Zara']);
|
||||||
|
function guessEthnicityFromFirstName(name){
|
||||||
|
if(!name) return 'caucasian';
|
||||||
|
var clean = name.replace(/[^A-Za-z]/g,'');
|
||||||
|
if(!clean) return 'caucasian';
|
||||||
|
var c = clean[0].toUpperCase() + clean.slice(1).toLowerCase();
|
||||||
|
// Order matters where names overlap. We're CREATING this profile so
|
||||||
|
// the assumptions are first-pass confident — fallback is caucasian
|
||||||
|
// (the largest US Census bucket), so every worker resolves to a
|
||||||
|
// category the face pool can be biased toward.
|
||||||
|
if(NAMES_MIDDLE_EASTERN.has(c)) return 'middle_eastern';
|
||||||
|
if(NAMES_BLACK.has(c)) return 'black';
|
||||||
|
if(NAMES_HISPANIC.has(c)) return 'hispanic';
|
||||||
|
if(NAMES_SOUTH_ASIAN.has(c)) return 'south_asian';
|
||||||
|
if(NAMES_EAST_ASIAN.has(c)) return 'east_asian';
|
||||||
|
return 'caucasian';
|
||||||
|
}
|
||||||
|
// Forced-confident gender resolver — defaults to a deterministic guess
|
||||||
|
// when the name table doesn't match, rather than leaving "unknown."
|
||||||
|
// We're authoring the synthetic data; we own the confidence call.
|
||||||
|
function genderFor(name){
|
||||||
|
var g = guessGenderFromFirstName(name);
|
||||||
|
if(g) return g;
|
||||||
|
if(!name) return 'man';
|
||||||
|
// hash-based fallback so unknown names still spread roughly 50/50
|
||||||
|
var s = String(name);
|
||||||
|
var h = 0; for (var i=0;i<s.length;i++) h = (h*31 + s.charCodeAt(i))|0;
|
||||||
|
return (Math.abs(h) & 1) ? 'man' : 'woman';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role classification — sober, no illustrations. Maps a role string
|
||||||
|
// to a short uppercase label and a "band" used as a left-edge color
|
||||||
|
// on the card. Five bands cover the warehouse/manufacturing surface;
|
||||||
|
// the band controls the left border on .iworker and on the role pill.
|
||||||
|
// No emojis — staffer-readable, professional. Add bands here when
|
||||||
|
// new role families appear in the data.
|
||||||
|
var ROLE_BANDS = [
|
||||||
|
{ match: /forklift|warehouse|associate|material\s*handler|loader|loading|packag|shipping|logistics|inventory|sanitation|janit/i,
|
||||||
|
band: 'warehouse', label: 'Warehouse' },
|
||||||
|
{ match: /production|assembl/i,
|
||||||
|
band: 'production', label: 'Production' },
|
||||||
|
{ match: /welder|weld|electric|maint(enance)?\s*tech|cnc|machine\s*op|hvac|plumb|carpenter|mason/i,
|
||||||
|
band: 'trades', label: 'Skilled Trade' },
|
||||||
|
{ match: /driver|truck|haul|cdl/i,
|
||||||
|
band: 'driver', label: 'Driver' },
|
||||||
|
{ match: /line\s*lead|supervisor|foreman|coordinator/i,
|
||||||
|
band: 'lead', label: 'Lead' },
|
||||||
|
{ match: /quality/i,
|
||||||
|
band: 'production', label: 'Quality' },
|
||||||
|
];
|
||||||
|
function roleBand(role){
|
||||||
|
if(!role) return { band: 'warehouse', label: '' };
|
||||||
|
for (var i = 0; i < ROLE_BANDS.length; i++) {
|
||||||
|
if (ROLE_BANDS[i].match.test(role)) return ROLE_BANDS[i];
|
||||||
|
}
|
||||||
|
return { band: 'warehouse', label: role.split(' ')[0].toUpperCase().slice(0, 12) };
|
||||||
|
}
|
||||||
|
|
||||||
function addWorkerInsight(parent,name,detail,why,idx,highlight){
|
function addWorkerInsight(parent,name,detail,why,idx,highlight){
|
||||||
var w=document.createElement('div');w.className='iworker';
|
var w=document.createElement('div');w.className='iworker';
|
||||||
if(highlight)w.style.borderLeft='3px solid '+highlight;
|
if(highlight)w.style.borderLeft='3px solid '+highlight;
|
||||||
@ -2198,8 +2376,33 @@ function addWorkerInsight(parent,name,detail,why,idx,highlight){
|
|||||||
var workerDataRef=arguments[6]||null; // passed as 7th arg
|
var workerDataRef=arguments[6]||null; // passed as 7th arg
|
||||||
var boostInfo=arguments[7]||null; // {boost, citations} — Phase 19
|
var boostInfo=arguments[7]||null; // {boost, citations} — Phase 19
|
||||||
w.onclick=function(){if(workerDataRef)showProfile(workerDataRef)};
|
w.onclick=function(){if(workerDataRef)showProfile(workerDataRef)};
|
||||||
var av=document.createElement('div');av.className='av';av.style.background=AC[(idx||0)%AC.length];
|
// Avatar: monogram initials underneath, real synthetic headshot on
|
||||||
|
// top via /headshots/<key>. Same key → same face by deterministic
|
||||||
|
// hash. If the image fails to load (face pool not yet fetched, CDN
|
||||||
|
// blocked, etc.), the monogram remains visible.
|
||||||
|
var av=document.createElement('div');av.className='av';
|
||||||
|
var role = (workerDataRef && workerDataRef.role) || (detail||'').split(' · ')[0] || '';
|
||||||
|
var band = roleBand(role);
|
||||||
|
if(band.band) w.dataset.roleBand = band.band;
|
||||||
av.textContent=(name||'?').split(' ').map(function(n){return(n[0]||'').toUpperCase()}).join('').substring(0,2);
|
av.textContent=(name||'?').split(' ').map(function(n){return(n[0]||'').toUpperCase()}).join('').substring(0,2);
|
||||||
|
// Layer the headshot on top. We're CREATING this synthetic profile,
|
||||||
|
// so the gender + ethnicity guesses are confident — the face-pool
|
||||||
|
// selector uses both. Once deepface tags the pool the server will
|
||||||
|
// narrow accordingly; until then it falls back to the full pool but
|
||||||
|
// the URL shape is forward-compatible.
|
||||||
|
var faceKey = (workerDataRef && (workerDataRef.candidate_id || workerDataRef.doc_id)) || name || '';
|
||||||
|
var firstName = (name||'').split(/\s+/)[0]||'';
|
||||||
|
var gHint = genderFor(firstName);
|
||||||
|
var eHint = guessEthnicityFromFirstName(firstName);
|
||||||
|
if(faceKey){
|
||||||
|
var img=document.createElement('img');
|
||||||
|
img.alt='';
|
||||||
|
img.loading='lazy';
|
||||||
|
var qs = '?g=' + gHint + '&e=' + eHint;
|
||||||
|
img.src = P + '/headshots/' + encodeURIComponent(faceKey) + qs;
|
||||||
|
img.onerror=function(){ this.remove(); };
|
||||||
|
av.appendChild(img);
|
||||||
|
}
|
||||||
w.appendChild(av);
|
w.appendChild(av);
|
||||||
var info=document.createElement('div');info.className='info';
|
var info=document.createElement('div');info.className='info';
|
||||||
var nm=document.createElement('div');nm.className='nm';nm.textContent=name;
|
var nm=document.createElement('div');nm.className='nm';nm.textContent=name;
|
||||||
@ -2234,7 +2437,17 @@ function addWorkerInsight(parent,name,detail,why,idx,highlight){
|
|||||||
chip.title=name+' — endorsed in '+n+' playbook'+(n!==1?'s':'');
|
chip.title=name+' — endorsed in '+n+' playbook'+(n!==1?'s':'');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var dt=document.createElement('div');dt.className='detail';dt.textContent=detail;
|
// Detail line. Lead with a small uppercase pill that classifies the
|
||||||
|
// role family — color matches the card's left-edge band so the eye
|
||||||
|
// groups by role family at a glance. No icons.
|
||||||
|
var dt=document.createElement('div');dt.className='detail';
|
||||||
|
if(band.label){
|
||||||
|
var pill=document.createElement('span'); pill.className='role-pill';
|
||||||
|
pill.dataset.rb = band.band;
|
||||||
|
pill.textContent = band.label;
|
||||||
|
dt.appendChild(pill);
|
||||||
|
}
|
||||||
|
dt.appendChild(document.createTextNode(detail));
|
||||||
info.appendChild(nm);info.appendChild(dt);
|
info.appendChild(nm);info.appendChild(dt);
|
||||||
if(why){var wh=document.createElement('div');wh.className='why';wh.textContent=why;info.appendChild(wh)}
|
if(why){var wh=document.createElement('div');wh.className='why';wh.textContent=why;info.appendChild(wh)}
|
||||||
w.appendChild(info);
|
w.appendChild(info);
|
||||||
@ -2270,19 +2483,155 @@ function pw(text){
|
|||||||
rel:rr?parseFloat(rr[1]):0,avail:av?parseFloat(av[1]):0,arch:ar?ar[1]:'',hasM:!!rr}
|
rel:rr?parseFloat(rr[1]):0,avail:av?parseFloat(av[1]):0,arch:ar?ar[1]:'',hasM:!!rr}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Type-specific result renderers ─────────────────────────────────────
|
||||||
|
function renderMiss(out,msg,color){
|
||||||
|
var d=document.createElement('div');
|
||||||
|
d.style.cssText='background:#0d1117;border:1px solid '+(color||'#21262d')+'66;border-left:3px solid '+(color||'#21262d')+';border-radius:6px;padding:14px 16px;color:#8b949e;font-size:13px;line-height:1.5';
|
||||||
|
d.textContent=msg;
|
||||||
|
out.appendChild(d);
|
||||||
|
}
|
||||||
|
function workerLine(w){
|
||||||
|
var bits=[];
|
||||||
|
if(w.role) bits.push(w.role);
|
||||||
|
if(w.city||w.state) bits.push((w.city||'')+(w.city&&w.state?', ':'')+(w.state||''));
|
||||||
|
if(w.zip) bits.push('ZIP '+w.zip);
|
||||||
|
return bits.join(' · ');
|
||||||
|
}
|
||||||
|
function appendStat(parent,label,val){
|
||||||
|
var s=document.createElement('span');
|
||||||
|
var l=document.createElement('span');l.textContent=label+': ';
|
||||||
|
var b=document.createElement('b');b.style.color='#e6edf3';b.textContent=val;
|
||||||
|
s.appendChild(l);s.appendChild(b);
|
||||||
|
parent.appendChild(s);
|
||||||
|
}
|
||||||
|
function renderTriage(out,d){
|
||||||
|
var w=d.worker, bf=d.backfills||[];
|
||||||
|
var card=document.createElement('div');
|
||||||
|
card.style.cssText='background:#1a1410;border:1px solid #d29922;border-left:3px solid #d29922;border-radius:8px;padding:16px;margin-bottom:14px';
|
||||||
|
var ev=document.createElement('div');
|
||||||
|
ev.style.cssText='font-size:11px;color:#d29922;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:6px';
|
||||||
|
ev.textContent='⚠ TRIAGE — '+(d.event||'event').toUpperCase();
|
||||||
|
card.appendChild(ev);
|
||||||
|
var hdr=document.createElement('div');
|
||||||
|
hdr.style.cssText='font-size:16px;color:#e6edf3;font-weight:600;margin-bottom:4px';
|
||||||
|
hdr.textContent=w.name;
|
||||||
|
card.appendChild(hdr);
|
||||||
|
var line=document.createElement('div');
|
||||||
|
line.style.cssText='font-size:12px;color:#8b949e;margin-bottom:10px';
|
||||||
|
line.textContent=workerLine(w);
|
||||||
|
card.appendChild(line);
|
||||||
|
var stats=document.createElement('div');
|
||||||
|
stats.style.cssText='font-size:11px;color:#8b949e;margin-bottom:10px;display:flex;gap:14px;flex-wrap:wrap';
|
||||||
|
appendStat(stats,'Reliability',Math.round((w.rel||0)*100)+'%');
|
||||||
|
appendStat(stats,'Responsiveness',Math.round((w.resp||0)*100)+'%');
|
||||||
|
appendStat(stats,'Availability',Math.round((w.avail||0)*100)+'%');
|
||||||
|
if(w.archetype) appendStat(stats,'Archetype',w.archetype);
|
||||||
|
if(w.recent_calls!=null) appendStat(stats,'Prior calls',w.recent_calls);
|
||||||
|
card.appendChild(stats);
|
||||||
|
var smsLabel=document.createElement('div');
|
||||||
|
smsLabel.style.cssText='font-size:10px;color:#d29922;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:4px';
|
||||||
|
smsLabel.textContent='DRAFT SMS — TO CLIENT';
|
||||||
|
card.appendChild(smsLabel);
|
||||||
|
var smsBox=document.createElement('div');
|
||||||
|
smsBox.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:10px 12px;font-family:ui-monospace,monospace;font-size:12px;color:#e6edf3;line-height:1.5;white-space:pre-wrap';
|
||||||
|
smsBox.textContent=d.draft_sms||'';
|
||||||
|
card.appendChild(smsBox);
|
||||||
|
var copyBtn=document.createElement('button');
|
||||||
|
copyBtn.style.cssText='margin-top:8px;background:#1f6feb;border:none;color:#fff;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer';
|
||||||
|
copyBtn.textContent='Copy SMS';
|
||||||
|
copyBtn.onclick=function(){
|
||||||
|
if(navigator.clipboard) navigator.clipboard.writeText(d.draft_sms||'');
|
||||||
|
copyBtn.textContent='Copied ✓';
|
||||||
|
setTimeout(function(){copyBtn.textContent='Copy SMS'},1500);
|
||||||
|
};
|
||||||
|
card.appendChild(copyBtn);
|
||||||
|
out.appendChild(card);
|
||||||
|
if(bf.length){
|
||||||
|
var bfHdr=document.createElement('div');
|
||||||
|
bfHdr.style.cssText='font-size:11px;color:#3fb950;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin:8px 0 8px';
|
||||||
|
bfHdr.textContent='✓ BACKFILLS READY — '+bf.length+' local '+(w.role||'workers')+' available, sorted by responsiveness';
|
||||||
|
out.appendChild(bfHdr);
|
||||||
|
bf.forEach(function(c,i){
|
||||||
|
addWorkerInsight(out,c.name,workerLine(c),
|
||||||
|
'Reliability '+Math.round((c.rel||0)*100)+'% · Responds '+Math.round((c.resp||0)*100)+'% · Available '+Math.round((c.avail||0)*100)+'%'+(c.archetype?' · '+c.archetype:''),
|
||||||
|
i,'#3fb950',c);
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
var bfNone=document.createElement('div');
|
||||||
|
bfNone.style.cssText='background:#1a1010;border:1px solid #f85149;border-radius:6px;padding:10px 14px;color:#fca5a5;font-size:12px';
|
||||||
|
bfNone.textContent='No same-role workers available locally. Widen the search — try a neighboring city or relax availability threshold.';
|
||||||
|
out.appendChild(bfNone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function renderProfiles(out,d){
|
||||||
|
var hdr=document.createElement('div');
|
||||||
|
hdr.style.cssText='font-size:12px;color:#8b949e;margin-bottom:10px';
|
||||||
|
hdr.textContent=d.summary;
|
||||||
|
out.appendChild(hdr);
|
||||||
|
(d.profiles||[]).forEach(function(w,i){
|
||||||
|
addWorkerInsight(out,w.name,workerLine(w),
|
||||||
|
'Reliability '+Math.round((w.rel||0)*100)+'%'+(w.resp?' · Responds '+Math.round(w.resp*100)+'%':'')+(w.archetype?' · '+w.archetype:''),
|
||||||
|
i,null,w);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderIngestLog(out,d){
|
||||||
|
var hdr=document.createElement('div');
|
||||||
|
hdr.style.cssText='font-size:12px;color:#e6edf3;margin-bottom:10px;padding:10px 12px;background:#0d2818;border:1px solid #2ea04340;border-left:3px solid #3fb950;border-radius:6px';
|
||||||
|
hdr.textContent=d.summary;
|
||||||
|
out.appendChild(hdr);
|
||||||
|
(d.datasets||[]).forEach(function(ds){
|
||||||
|
var card=document.createElement('div');
|
||||||
|
card.style.cssText='background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:12px 14px;margin-bottom:8px';
|
||||||
|
var top=document.createElement('div');
|
||||||
|
top.style.cssText='display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px';
|
||||||
|
var nm=document.createElement('span');
|
||||||
|
nm.style.cssText='font-size:13px;color:#e6edf3;font-weight:600';
|
||||||
|
nm.textContent=ds.name;
|
||||||
|
var ago=document.createElement('span');
|
||||||
|
ago.style.cssText='font-size:11px;color:#545d68';
|
||||||
|
ago.textContent=(ds.hours_ago||0)+'h ago · '+(ds.row_count||0).toLocaleString()+' rows';
|
||||||
|
top.appendChild(nm);top.appendChild(ago);
|
||||||
|
card.appendChild(top);
|
||||||
|
if(ds.looks_like_workers && ds.role_breakdown && ds.role_breakdown.length){
|
||||||
|
var rb=document.createElement('div');
|
||||||
|
rb.style.cssText='font-size:11px;color:#8b949e;display:flex;gap:10px;flex-wrap:wrap;margin-top:4px';
|
||||||
|
ds.role_breakdown.forEach(function(r){
|
||||||
|
var pill=document.createElement('span');
|
||||||
|
pill.style.cssText='background:#161b22;border:1px solid #21262d;padding:2px 8px;border-radius:9px';
|
||||||
|
pill.textContent=(r.role||'?')+' · '+r.cnt;
|
||||||
|
rb.appendChild(pill);
|
||||||
|
});
|
||||||
|
card.appendChild(rb);
|
||||||
|
}
|
||||||
|
out.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function doSearch(){
|
function doSearch(){
|
||||||
var q=document.getElementById('sq').value.trim();if(!q)return;
|
var q=document.getElementById('sq').value.trim();if(!q)return;
|
||||||
lastQuery=q;
|
lastQuery=q;
|
||||||
var st=document.getElementById('sst').value,rl=document.getElementById('srl').value;
|
var st=document.getElementById('sst').value,rl=document.getElementById('srl').value;
|
||||||
// Append dropdown filters to the query so the smart parser picks them up
|
var stafferEl=document.getElementById('sstaffer');
|
||||||
var fullQ=q;
|
var stafferId=stafferEl?stafferEl.value:'';
|
||||||
if(st&&q.indexOf(st)<0)fullQ+=' in '+st;
|
// Pass dropdown filters as structured fields. Old code appended
|
||||||
if(rl&&q.toLowerCase().indexOf(rl.toLowerCase())<0)fullQ+=' '+rl;
|
// ' in '+st to the message, which the server misparsed: the
|
||||||
|
// preposition "in" matched the regex for state code "IN" (Indiana)
|
||||||
|
// and every search returned Indiana workers regardless of dropdown.
|
||||||
|
// Sending structured state/role + staffer_id lets the server skip
|
||||||
|
// NL parsing for those fields and apply per-staffer scoping.
|
||||||
var out=document.getElementById('sresults');out.textContent='Finding the best matches...';
|
var out=document.getElementById('sresults');out.textContent='Finding the best matches...';
|
||||||
fetch(A+'/intelligence/chat',{method:'POST',headers:{'Content-Type':'application/json'},
|
fetch(A+'/intelligence/chat',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||||
body:JSON.stringify({message:fullQ})
|
body:JSON.stringify({message:q,state:st||undefined,role:rl||undefined,staffer_id:stafferId||undefined})
|
||||||
}).then(function(r){return r.json()}).then(function(d){
|
}).then(function(r){return r.json()}).then(function(d){
|
||||||
out.textContent='';
|
out.textContent='';
|
||||||
|
// Type-specific renderers — added 2026-04-27 for the persona-driven
|
||||||
|
// routes (triage / profile / ingest_log). Default falls through to
|
||||||
|
// the smart_search renderer below.
|
||||||
|
if(d.type==='triage' && d.worker){return renderTriage(out,d)}
|
||||||
|
if(d.type==='triage_miss'){return renderMiss(out,d.summary,'#f85149')}
|
||||||
|
if(d.type==='profile' && d.profiles && d.profiles.length){return renderProfiles(out,d)}
|
||||||
|
if(d.type==='profile_miss'){return renderMiss(out,d.summary,'#d29922')}
|
||||||
|
if(d.type==='ingest_log'){return renderIngestLog(out,d)}
|
||||||
// Show what the system understood
|
// Show what the system understood
|
||||||
if(d.understood&&d.understood.length){
|
if(d.understood&&d.understood.length){
|
||||||
var tags=document.createElement('div');tags.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px';
|
var tags=document.createElement('div');tags.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px';
|
||||||
@ -2304,7 +2653,11 @@ function doSearch(){
|
|||||||
var mem=document.createElement('div');
|
var mem=document.createElement('div');
|
||||||
mem.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5';
|
mem.style.cssText='background:#0d2818;border:1px solid #2ea04360;border-radius:6px;padding:8px 12px;margin-bottom:10px;font-size:11px;color:#86efac;line-height:1.5';
|
||||||
var label=document.createElement('span');label.style.cssText='color:#3fb950;font-weight:600;margin-right:6px';
|
var label=document.createElement('span');label.style.cssText='color:#3fb950;font-weight:600;margin-right:6px';
|
||||||
label.textContent='MEMORY ('+(d.pattern_playbooks_matched||0)+' playbook'+(d.pattern_playbooks_matched===1?'':'s')+'):';
|
// When a staffer is acting, label the panel with their name —
|
||||||
|
// "MARIA'S MEMORY (12 playbooks)" makes the per-user shaping
|
||||||
|
// visible in the UI, not just the response data.
|
||||||
|
var memOwner=d.staffer&&d.staffer.name?d.staffer.name.toUpperCase()+"'S MEMORY":'MEMORY';
|
||||||
|
label.textContent=memOwner+' ('+(d.pattern_playbooks_matched||0)+' playbook'+(d.pattern_playbooks_matched===1?'':'s')+'):';
|
||||||
mem.appendChild(label);
|
mem.appendChild(label);
|
||||||
var pattern = d.discovered_pattern || '';
|
var pattern = d.discovered_pattern || '';
|
||||||
if(!pattern || pattern.indexOf('No similar')>=0 || pattern.indexOf('0 workers')>=0){
|
if(!pattern || pattern.indexOf('No similar')>=0 || pattern.indexOf('0 workers')>=0){
|
||||||
@ -2500,60 +2853,228 @@ function flashBtn(btn,label){
|
|||||||
function logSelection(workerData){ logAction(workerData, 'call', null); }
|
function logSelection(workerData){ logAction(workerData, 'call', null); }
|
||||||
|
|
||||||
function loadLearning(){
|
function loadLearning(){
|
||||||
api('/intelligence/activity',{}).then(function(d){
|
// Pull every capability's live metric in parallel. Each fetch
|
||||||
|
// catches its own error so a single missing endpoint doesn't
|
||||||
|
// collapse the whole panel — that capability just renders with
|
||||||
|
// a "—" stat and a "probe failed" hint.
|
||||||
|
var probes = [
|
||||||
|
fetch(P+'/staffers').then(function(r){return r.json()}).catch(function(){return null}),
|
||||||
|
fetch(P+'/system/summary').then(function(r){return r.json()}).catch(function(){return null}),
|
||||||
|
fetch(A+'/api/vectors/playbook_memory/stats').then(function(r){return r.json()}).catch(function(){return null}),
|
||||||
|
fetch(A+'/api/vectors/pathway/stats').then(function(r){return r.json()}).catch(function(){return null}),
|
||||||
|
fetch(P+'/intelligence/profiler_index',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({limit:200})}).then(function(r){return r.json()}).catch(function(){return null}),
|
||||||
|
api('/intelligence/activity',{}).catch(function(){return null}),
|
||||||
|
];
|
||||||
|
Promise.all(probes).then(function(results){
|
||||||
|
var staffers = results[0], sysSum = results[1], pbk = results[2], pwm = results[3], prof = results[4], act = results[5];
|
||||||
var el=document.getElementById('learning');
|
var el=document.getElementById('learning');
|
||||||
el.textContent='';
|
el.textContent='';
|
||||||
var total=d.total_operations||0;
|
var grid=document.createElement('div');
|
||||||
if(total===0&&(!d.playbooks||!d.playbooks.length))return; // nothing to show yet
|
grid.style.cssText='display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:12px';
|
||||||
|
el.appendChild(grid);
|
||||||
|
|
||||||
var card=document.createElement('div');card.className='insight info';
|
// ─── 1. Per-staffer hot-swap index ──────────────────────────
|
||||||
var lb=document.createElement('div');lb.className='label';lb.textContent='SYSTEM LEARNING';
|
var stafferCount = (staffers && staffers.staffers ? staffers.staffers.length : 0);
|
||||||
var hl=document.createElement('div');hl.className='headline';hl.textContent='The System Gets Smarter With Every Use';
|
capability(grid, {
|
||||||
var sub=document.createElement('div');sub.className='sub';
|
shipped:'2026-04-27',
|
||||||
sub.textContent='Every search, every placement, every simulation teaches the system what works. '+total+' operations logged so far.';
|
kind:'staffer-index',
|
||||||
card.appendChild(lb);card.appendChild(hl);card.appendChild(sub);
|
title:'Per-staffer hot-swap index',
|
||||||
|
stat: stafferCount + ' personas',
|
||||||
|
sub: stafferCount ? staffers.staffers.map(function(s){return s.name}).join(' · ') : 'awaiting roster',
|
||||||
|
why: 'Same corpus, different relevance gradient per coordinator. MARIA\'S MEMORY pill labels playbook context with the staffer\'s territory; same query returns different rosters depending on who\'s acting.',
|
||||||
|
kindColor: '#58a6ff',
|
||||||
|
});
|
||||||
|
|
||||||
// Stats row
|
// ─── 2. Construction Activity Signal Engine ─────────────────
|
||||||
var stats=document.createElement('div');stats.style.cssText='display:flex;gap:16px;margin-bottom:12px';
|
var basket = (prof && prof.contractors) ? aggregateBasket(prof.contractors) : [];
|
||||||
addLearnStat(stats,d.fill_count||0,'Contract Fills','#3fb950');
|
var attribCost = (prof && prof.contractors)
|
||||||
addLearnStat(stats,d.search_count||0,'Searches','#58a6ff');
|
? prof.contractors.filter(function(r){
|
||||||
addLearnStat(stats,(d.learned_patterns||[]).length,'Patterns','#bc8cff');
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||||||
card.appendChild(stats);
|
return ts.length>0;
|
||||||
|
}).reduce(function(s,r){return s+(r.total_cost||0)},0)
|
||||||
|
: 0;
|
||||||
|
capability(grid, {
|
||||||
|
shipped:'2026-04-27',
|
||||||
|
kind:'signal-engine',
|
||||||
|
title:'Construction Activity Signal Engine',
|
||||||
|
stat: basket.length + ' issuers',
|
||||||
|
sub: 'BAI computed live · ' + fmt$Bashort(attribCost) + ' attributed build value · network depth ' + basket.length + ' / ' + basket.reduce(function(s,b){return s+(b.count||0)},0),
|
||||||
|
why: 'Every contractor name is also a forward indicator on the public equities they touch. Permit activity → construction starts ~45d → revenue recognition months later. Pre-10-Q signal.',
|
||||||
|
link: P+'/profiler',
|
||||||
|
kindColor: '#3fb950',
|
||||||
|
});
|
||||||
|
|
||||||
// Learned patterns
|
// ─── 3. Late-worker / no-show triage ────────────────────────
|
||||||
if(d.learned_patterns&&d.learned_patterns.length){
|
capability(grid, {
|
||||||
var ph=document.createElement('div');ph.style.cssText='font-size:12px;font-weight:600;color:#f0f6fc;margin-bottom:6px';
|
shipped:'2026-04-27',
|
||||||
ph.textContent='Learned Search Patterns';card.appendChild(ph);
|
kind:'route',
|
||||||
d.learned_patterns.slice(0,5).forEach(function(p){
|
title:'Late-worker / no-show triage',
|
||||||
var row=document.createElement('div');row.style.cssText='display:flex;justify-content:space-between;padding:5px 10px;background:#0d1117;border-radius:6px;margin-bottom:3px;font-size:12px';
|
stat:'one-shot',
|
||||||
var q=document.createElement('span');q.style.color='#c9d1d9';q.textContent='"'+p.query+'"';
|
sub:'name + "running late" → profile + reliability + 5 backfills sorted by responsiveness + draft SMS to client',
|
||||||
var c=document.createElement('span');c.style.cssText='color:#58a6ff;font-weight:600';c.textContent=p.times+'x';
|
why: 'A coordinator gets a text and the system pulls the worker, finds backfills, and pre-writes the message in 250ms. Turns 20 minutes into 2.',
|
||||||
row.appendChild(q);row.appendChild(c);card.appendChild(row);
|
kindColor: '#d29922',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── 4. Permit → staffing bridge (live contracts) ───────────
|
||||||
|
var permitCount = (act && typeof act.permits_today === 'number') ? act.permits_today : null;
|
||||||
|
capability(grid, {
|
||||||
|
shipped:'2026-04-27',
|
||||||
|
kind:'permit-bridge',
|
||||||
|
title:'Permit → staffing bridge',
|
||||||
|
stat: '24 / day',
|
||||||
|
sub: 'every Chicago permit ≥$250K becomes a fill plan — role, headcount, deadline, fill probability, gross revenue, draft SMS. ' + (permitCount==null ? 'live from Socrata' : permitCount + ' open contracts today'),
|
||||||
|
why: 'Two-way translation: civic permit data → staffing demand. Same adapter pattern works for NYC DOB, LA County, Houston BCD.',
|
||||||
|
kindColor: '#bc8cff',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 5. Hybrid SQL+vector search ────────────────────────────
|
||||||
|
var workersCount = sysSum && typeof sysSum.workers_500k_rows === 'number' ? sysSum.workers_500k_rows : null;
|
||||||
|
var pbkEntries = pbk && typeof pbk.entries === 'number' ? pbk.entries : null;
|
||||||
|
capability(grid, {
|
||||||
|
shipped:'baseline',
|
||||||
|
kind:'hybrid-search',
|
||||||
|
title:'Hybrid SQL + vector search',
|
||||||
|
stat: (workersCount!=null ? (workersCount/1000).toFixed(0)+'K' : '—') + ' workers',
|
||||||
|
sub: (pbkEntries!=null ? pbkEntries.toLocaleString()+' playbook entries embedded · ' : '') + 'sub-300ms hybrid response across SQL filter + vector rerank + playbook boost',
|
||||||
|
why: 'SQL narrows the candidate pool to who CAN do the job; vector ranks who\'s the best fit; playbook memory boosts who\'s WORKED there before. All in one call.',
|
||||||
|
kindColor: '#58a6ff',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 6. Schema-agnostic ingestion ───────────────────────────
|
||||||
|
var datasets = sysSum ? sysSum.datasets : null;
|
||||||
|
var rows = sysSum ? sysSum.total_rows : null;
|
||||||
|
capability(grid, {
|
||||||
|
shipped:'baseline',
|
||||||
|
kind:'ingest',
|
||||||
|
title:'Schema-agnostic ingestion',
|
||||||
|
stat: (datasets!=null ? datasets : '—') + ' datasets',
|
||||||
|
sub: (rows!=null ? rows.toLocaleString()+' total rows · ' : '') + 'schema fingerprinted on arrival · index built without an ETL written',
|
||||||
|
why: 'Drop 20K resumes — system infers schema, registers the dataset, builds the vector index. No mapping spec, no ETL ticket, no DBA. Same path for any tabular feed in any new metro.',
|
||||||
|
kindColor: '#79c0ff',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 7. Contractor profile + 12 awaiting sources ────────────
|
||||||
|
capability(grid, {
|
||||||
|
shipped:'2026-04-27',
|
||||||
|
kind:'contractor-profile',
|
||||||
|
title:'Contractor profile + project index',
|
||||||
|
stat:'6 wired · 12 queued',
|
||||||
|
sub:'OSHA · SEC+Stooq · Chicago history (lat/lng) · USAspending · parent-ticker map · ILSOS. Each name is also a heat-map of where they work.',
|
||||||
|
why: 'Profile every contractor in the corpus with project-index score, geo heat map, history timeline, and 12 placeholder cards naming the next public datasets that ship as adapters.',
|
||||||
|
link: P+'/contractor?name=Turner+Construction+Company',
|
||||||
|
kindColor: '#d29922',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 8. Pathway memory ──────────────────────────────────────
|
||||||
|
var pwmTotal = pwm && typeof pwm.total_pathways === 'number' ? pwm.total_pathways : null;
|
||||||
|
var pwmReplays = pwm && typeof pwm.successful_replays === 'number' ? pwm.successful_replays : null;
|
||||||
|
var pwmTotal2 = pwm && typeof pwm.total_replays === 'number' ? pwm.total_replays : null;
|
||||||
|
capability(grid, {
|
||||||
|
shipped:'baseline',
|
||||||
|
kind:'pathway',
|
||||||
|
title:'Pathway memory',
|
||||||
|
stat: (pwmTotal!=null ? pwmTotal : '—') + ' traces',
|
||||||
|
sub: pwmReplays!=null ? (pwmReplays + ' / ' + pwmTotal2 + ' successful replays · ' + (pwmTotal2 ? Math.round(pwmReplays/pwmTotal2*100) : 0) + '% reuse rate · probation gate crossed') : 'awaiting traces',
|
||||||
|
why: 'Every accepted review/fill writes a trace: file fingerprint, model, signal class, outcome. A new query that fingerprints to the same trace gets the prior result without re-running the 9-rung escalation.',
|
||||||
|
kindColor: '#3fb950',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── 9. Profiler ticker basket ──────────────────────────────
|
||||||
|
var directIssuers = basket.filter(function(b){return b.kinds && (b.kinds.indexOf('exact')>=0 || b.kinds.indexOf('direct')>=0)}).length;
|
||||||
|
var assocIssuers = basket.filter(function(b){return b.kinds && b.kinds.indexOf('associated')>=0}).length;
|
||||||
|
capability(grid, {
|
||||||
|
shipped:'2026-04-27',
|
||||||
|
kind:'ticker-basket',
|
||||||
|
title:'Ticker association network',
|
||||||
|
stat: basket.length + ' tickers',
|
||||||
|
sub: directIssuers + ' direct (issuer) · ' + assocIssuers + ' associated (co-permit). Live Stooq quotes · clickable basket above the profiler index.',
|
||||||
|
why: 'When a contractor co-files permits with TARGET CORPORATION, that contractor inherits TGT as an associated indicator. The network is the moat — every new metro multiplies the edges.',
|
||||||
|
link: P+'/profiler',
|
||||||
|
kindColor: '#58a6ff',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optional bottom row: operational stats from /intelligence/activity
|
||||||
|
if(act && (act.fill_count || act.search_count || (act.playbooks||[]).length)){
|
||||||
|
var opRow = document.createElement('div');
|
||||||
|
opRow.style.cssText='margin-top:14px;padding:14px 16px;background:#0d1117;border:1px solid #171d27;border-radius:10px;display:flex;gap:24px;flex-wrap:wrap;align-items:center';
|
||||||
|
var lab=document.createElement('span'); lab.style.cssText='font-size:10px;color:#545d68;text-transform:uppercase;letter-spacing:1.4px;font-weight:600';
|
||||||
|
lab.textContent='Operational learning';
|
||||||
|
opRow.appendChild(lab);
|
||||||
|
var fc = document.createElement('span'); fc.style.cssText='color:#e6edf3;font-size:13px';
|
||||||
|
fc.textContent = (act.fill_count||0)+' fills · '+(act.search_count||0)+' searches · '+((act.playbooks||[]).length)+' recent playbooks';
|
||||||
|
opRow.appendChild(fc);
|
||||||
|
var note=document.createElement('span'); note.style.cssText='color:#545d68;font-size:11px;flex:1';
|
||||||
|
note.textContent='compounds inside each capability above';
|
||||||
|
opRow.appendChild(note);
|
||||||
|
el.appendChild(opRow);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Recent activity feed
|
// Helper — build basket from contractor rows (mirrors profiler.html logic)
|
||||||
if(d.playbooks&&d.playbooks.length){
|
function aggregateBasket(rows){
|
||||||
var ah=document.createElement('div');ah.style.cssText='font-size:12px;font-weight:600;color:#f0f6fc;margin:10px 0 6px';
|
var byTicker = {};
|
||||||
ah.textContent='Recent Activity';card.appendChild(ah);
|
rows.forEach(function(r){
|
||||||
d.playbooks.slice(0,5).forEach(function(p){
|
var ts=(r.tickers&&r.tickers.direct?r.tickers.direct:[]).concat(r.tickers&&r.tickers.associated?r.tickers.associated:[]);
|
||||||
var row=document.createElement('div');row.style.cssText='padding:6px 10px;background:#0d1117;border-radius:6px;margin-bottom:3px;font-size:11px';
|
ts.forEach(function(t){
|
||||||
var op=document.createElement('div');op.style.color='#f0f6fc';op.textContent=p.operation||'';
|
if(!t || !t.ticker) return;
|
||||||
var det=document.createElement('div');det.style.cssText='color:#484f58;margin-top:2px';
|
if(!byTicker[t.ticker]) byTicker[t.ticker]={ticker:t.ticker, kinds:[], count:0};
|
||||||
det.textContent=(p.result||'')+(p.context?' · '+p.context:'');
|
if(byTicker[t.ticker].kinds.indexOf(t.via)<0) byTicker[t.ticker].kinds.push(t.via);
|
||||||
var ts=document.createElement('div');ts.style.cssText='color:#2d333b;font-size:10px;margin-top:2px';
|
byTicker[t.ticker].count++;
|
||||||
ts.textContent=p.timestamp?new Date(p.timestamp).toLocaleString():'';
|
});
|
||||||
row.appendChild(op);row.appendChild(det);row.appendChild(ts);card.appendChild(row);
|
});
|
||||||
});
|
return Object.values(byTicker);
|
||||||
}
|
}
|
||||||
|
function fmt$Bashort(n){
|
||||||
// Explainer
|
if(!n) return '$0';
|
||||||
var ex=document.createElement('div');ex.style.cssText='font-size:11px;color:#484f58;margin-top:10px;font-style:italic;padding:8px;background:#0d1117;border-radius:6px';
|
if(n>=1e9) return '$'+(n/1e9).toFixed(2)+'B';
|
||||||
ex.textContent='Every time you search and select a worker, the system records what worked. Over time, it learns which workers are best for which situations — turning your decisions into institutional knowledge that never leaves when a staffer does.';
|
if(n>=1e6) return '$'+(n/1e6).toFixed(0)+'M';
|
||||||
card.appendChild(ex);
|
if(n>=1e3) return '$'+(n/1e3).toFixed(0)+'K';
|
||||||
|
return '$'+Math.round(n);
|
||||||
el.appendChild(card);
|
}
|
||||||
}).catch(function(){});
|
// Render one capability card
|
||||||
|
function capability(parent, c){
|
||||||
|
var card=document.createElement('div');
|
||||||
|
card.style.cssText='background:#0d1117;border:1px solid #171d27;border-left:3px solid '+(c.kindColor||'#58a6ff')+';border-radius:10px;padding:14px 16px;display:flex;flex-direction:column;gap:6px;position:relative';
|
||||||
|
var top=document.createElement('div');
|
||||||
|
top.style.cssText='display:flex;align-items:baseline;justify-content:space-between;gap:8px';
|
||||||
|
var title=document.createElement('div');
|
||||||
|
title.style.cssText='font-size:13px;font-weight:600;color:#e6edf3';
|
||||||
|
title.textContent=c.title;
|
||||||
|
top.appendChild(title);
|
||||||
|
if(c.shipped){
|
||||||
|
var ship=document.createElement('span');
|
||||||
|
ship.style.cssText='font-size:9px;color:#3fb950;background:#0d2818;border:1px solid #2ea04344;padding:1px 7px;border-radius:8px;letter-spacing:0.4px;font-weight:600;text-transform:uppercase;white-space:nowrap';
|
||||||
|
ship.textContent = c.shipped==='baseline' ? 'baseline' : 'shipped '+c.shipped;
|
||||||
|
top.appendChild(ship);
|
||||||
|
}
|
||||||
|
card.appendChild(top);
|
||||||
|
if(c.stat){
|
||||||
|
var stat=document.createElement('div');
|
||||||
|
stat.style.cssText='font-size:24px;font-weight:700;color:'+(c.kindColor||'#e6edf3')+';letter-spacing:-0.5px;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums';
|
||||||
|
stat.textContent=c.stat;
|
||||||
|
card.appendChild(stat);
|
||||||
|
}
|
||||||
|
if(c.sub){
|
||||||
|
var sub=document.createElement('div');
|
||||||
|
sub.style.cssText='font-size:11px;color:#8b949e;line-height:1.5';
|
||||||
|
sub.textContent=c.sub;
|
||||||
|
card.appendChild(sub);
|
||||||
|
}
|
||||||
|
if(c.why){
|
||||||
|
var why=document.createElement('div');
|
||||||
|
why.style.cssText='font-size:11px;color:#c9d1d9;line-height:1.55;margin-top:4px;border-top:1px dashed #1f2631;padding-top:8px';
|
||||||
|
why.textContent=c.why;
|
||||||
|
card.appendChild(why);
|
||||||
|
}
|
||||||
|
if(c.link){
|
||||||
|
var lk=document.createElement('a');
|
||||||
|
lk.href=c.link; lk.target='_blank'; lk.rel='noopener';
|
||||||
|
lk.style.cssText='color:#58a6ff;text-decoration:none;font-size:11px;font-weight:600;margin-top:4px;align-self:flex-start;border-bottom:1px dotted #58a6ff44';
|
||||||
|
lk.textContent='Open →';
|
||||||
|
card.appendChild(lk);
|
||||||
|
}
|
||||||
|
parent.appendChild(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLearnStat(parent,n,label,color){
|
function addLearnStat(parent,n,label,color){
|
||||||
|
|||||||
@ -78,13 +78,14 @@ table.plain tr:hover td{background:#0d1117}
|
|||||||
<nav>
|
<nav>
|
||||||
<a href=".">Dashboard</a>
|
<a href=".">Dashboard</a>
|
||||||
<a href="console">Walkthrough</a>
|
<a href="console">Walkthrough</a>
|
||||||
|
<a href="profiler">Profiler</a>
|
||||||
<a href="proof">Architecture</a>
|
<a href="proof">Architecture</a>
|
||||||
<a href="spec" class="active">Spec</a>
|
<a href="spec" class="active">Spec</a>
|
||||||
<a href="onboard">Onboard</a>
|
<a href="onboard">Onboard</a>
|
||||||
<a href="alerts">Alerts</a>
|
<a href="alerts">Alerts</a>
|
||||||
<a href="workspaces">Workspaces</a>
|
<a href="workspaces">Workspaces</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="rt">v1 · 2026-04-20</div>
|
<div class="rt">v3 · 2026-04-27</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
@ -120,14 +121,18 @@ table.plain tr:hover td{background:#0d1117}
|
|||||||
<tr><td class="mono">crates/vectord/</td><td>The vector + learning surface. Embeddings stored as Parquet (ADR-008), HNSW index (Phase 15), trial system (autotune), promotion registry (Phase 16), playbook_memory (Phase 19). Core feedback loop lives here.</td></tr>
|
<tr><td class="mono">crates/vectord/</td><td>The vector + learning surface. Embeddings stored as Parquet (ADR-008), HNSW index (Phase 15), trial system (autotune), promotion registry (Phase 16), playbook_memory (Phase 19). Core feedback loop lives here.</td></tr>
|
||||||
<tr><td class="mono">crates/vectord-lance/</td><td>Firewall crate. Lance 4.0 + Arrow 57, isolated from the main Arrow-55 workspace. Provides secondary vector backend for large-scale, random-access, and append-heavy workloads (ADR-019).</td></tr>
|
<tr><td class="mono">crates/vectord-lance/</td><td>Firewall crate. Lance 4.0 + Arrow 57, isolated from the main Arrow-55 workspace. Provides secondary vector backend for large-scale, random-access, and append-heavy workloads (ADR-019).</td></tr>
|
||||||
<tr><td class="mono">crates/journald/</td><td>Append-only mutation event log (ADR-012). Every insert/update/delete writes here — who, when, what, old/new value. Never mutated. Foundation for time-travel + compliance audit.</td></tr>
|
<tr><td class="mono">crates/journald/</td><td>Append-only mutation event log (ADR-012). Every insert/update/delete writes here — who, when, what, old/new value. Never mutated. Foundation for time-travel + compliance audit.</td></tr>
|
||||||
<tr><td class="mono">crates/aibridge/</td><td>Rust ↔ Python sidecar. HTTP client over FastAPI wrapper around Ollama. VRAM introspection via nvidia-smi. All LLM calls (embed, generate, rerank) flow through here.</td></tr>
|
<tr><td class="mono">crates/truth/</td><td>File-backed rule store. <code>evaluate(task_class, ctx) → Vec<RuleOutcome></code> (ADR-021 — semantic-correctness matrix layer). Loaded from <code>truth/*.toml</code> at gateway boot.</td></tr>
|
||||||
<tr><td class="mono">crates/gateway/</td><td>Axum HTTP (:3100) + gRPC (:3101). Auth middleware, tools registry (Phase 12 — governed actions), CORS. Every external request enters here.</td></tr>
|
<tr><td class="mono">crates/aibridge/</td><td>Rust ↔ Python sidecar + provider adapter trait. HTTP client over FastAPI wrapper around Ollama for local; <code>ProviderAdapter</code> dispatch for cloud (ollama_cloud, openrouter, opencode, kimi). VRAM introspection via nvidia-smi. All LLM calls flow through here.</td></tr>
|
||||||
|
<tr><td class="mono">crates/gateway/</td><td>Axum HTTP (:3100) + gRPC (:3101). OpenAI-compat <code>/v1/*</code> (drop-in middleware), mode runner (<code>/v1/mode/execute</code>), validator (<code>/v1/validate</code>), iterate loop (<code>/v1/iterate</code>), tools registry, cost telemetry, Langfuse + observer fan-out on every chat. Every external request enters here.</td></tr>
|
||||||
|
<tr><td class="mono">crates/validator/</td><td>Phase 43 production validator. Schema / completeness / consistency / policy gates over LLM outputs. <code>FillValidator</code>, <code>EmailValidator</code>, <code>ParquetWorkerLookup</code> (loads workers_500k.parquet at boot). Fail-closed when roster absent.</td></tr>
|
||||||
<tr><td class="mono">crates/ui/</td><td>Dioxus WASM developer UI. Internal tool. Not exposed externally.</td></tr>
|
<tr><td class="mono">crates/ui/</td><td>Dioxus WASM developer UI. Internal tool. Not exposed externally.</td></tr>
|
||||||
<tr><td class="mono">mcp-server/</td><td>Bun/TypeScript recruiter-facing app. Serves <code>devop.live/lakehouse</code>. Routes: <code>/search /match /log /log_failure /clients/:c/blacklist /intelligence/* /memory/query /models/matrix /system/summary</code>. Observer sibling at <code>observer.ts</code> with HTTP listener on :3800 for scenario event ingest. Proxies to the Rust gateway for heavy work.</td></tr>
|
<tr><td class="mono">mcp-server/</td><td>Bun/TypeScript public-facing app + MCP tool surface. Serves <code>devop.live/lakehouse</code>. Pages: dashboard / console / profiler / contractor / proof / spec / onboard / alerts / workspaces. Routes: <code>/search /match /log /log_failure /clients/:c/blacklist /intelligence/* /staffers /memory/query /models/matrix /system/summary</code>. Observer sibling at <code>observer.ts</code> on :3800 for event ingest.</td></tr>
|
||||||
<tr><td class="mono">tests/multi-agent/</td><td>Dual-agent scenario harness + memory stack. <code>agent.ts</code> (prompts, continuation + tree-split primitives, cloud routing), <code>orchestrator.ts</code>, <code>scenario.ts</code> (contracts + staffer + tool_level), <code>kb.ts</code> (KB indexing, competence scoring, neighbor retrieval), <code>normalize.ts</code> (input normalizer — structured / regex / LLM), <code>memory_query.ts</code> (unified /memory/query), <code>gen_scenarios.ts</code> + <code>gen_staffer_demo.ts</code> (corpus generators), <code>run_e2e_rated.ts</code>, <code>chain_of_custody.ts</code>. Unit tests colocated (<code>kb.test.ts</code>, <code>normalize.test.ts</code>).</td></tr>
|
<tr><td class="mono">auditor/</td><td>External claim-vs-diff verifier on PRs. Polls Gitea for open PRs, builds adversarial prompt from PRD invariants + staffing matrix, alternates Kimi K2.6 ↔ Haiku 4.5 by SHA, auto-promotes Claude Opus 4.7 on diffs >100k chars. Per-PR cap=3 with auto-reset on each new head SHA. Verdicts at <code>data/_auditor/kimi_verdicts/</code>.</td></tr>
|
||||||
<tr><td class="mono">config/</td><td><code>models.json</code> — authoritative 5-tier model matrix (T1 hot local / T2 review local / T3 overview cloud / T4 strategic / T5 gatekeeper). Per-tier context_window + context_budget + overflow_policy. Read at runtime by scenario.ts; hot-swap friendly.</td></tr>
|
<tr><td class="mono">tests/multi-agent/</td><td>Multi-agent scenario harness + memory stack. <code>agent.ts</code>, <code>scenario.ts</code> (contracts + staffer + tool_level), <code>kb.ts</code> (KB indexing, competence scoring), <code>normalize.ts</code>, <code>memory_query.ts</code>, <code>run_e2e_rated.ts</code>. Unit tests colocated.</td></tr>
|
||||||
<tr><td class="mono">docs/</td><td><code>PRD.md</code>, <code>PHASES.md</code>, <code>DECISIONS.md</code> (20 ADRs). Every significant architectural choice has an ADR with the alternatives that were rejected and why.</td></tr>
|
<tr><td class="mono">scripts/distillation/</td><td>Distillation substrate v1.0.0 (frozen at tag <code>distillation-v1.0.0</code> / commit <code>e7636f2</code>). 145 unit tests, 22/22 acceptance, 16/16 audit-full, bit-identical reproducibility. Multi-layer contamination firewall on SFT exports.</td></tr>
|
||||||
<tr><td class="mono">data/</td><td>Default local object store. Parquet files per dataset, append-log batches, HNSW trial journals, promotion registries, <code>_playbook_memory/state.json</code> (now with retirement fields — Phase 25), catalog manifests. Plus four learning-loop directories: <code>_kb/</code> (signatures, outcomes, recommendations, error_corrections, config_snapshots, staffers), <code>_playbook_lessons/</code> (T3 cross-day lessons archived per run), <code>_observer/ops.jsonl</code> (append journal, durable scenario outcome stream), <code>_chunk_cache/</code> (spec'd for Phase 21 Rust port). Rebuildable from repo + this dir alone.</td></tr>
|
<tr><td class="mono">config/</td><td><code>modes.toml</code> — task_class → mode/model router (<code>scrum_review</code>, <code>contract_analysis</code>, <code>staffing_inference</code>, <code>pr_audit</code>, <code>doc_drift_check</code>, <code>fact_extract</code>). <code>providers.toml</code> — 5 active providers (ollama, ollama_cloud, openrouter, opencode 40-model, kimi direct). <code>routing.toml</code> — cost gates per task class.</td></tr>
|
||||||
|
<tr><td class="mono">docs/</td><td><code>PRD.md</code>, <code>PHASES.md</code>, <code>DECISIONS.md</code> (21 ADRs). Every significant architectural choice has an ADR with the alternatives that were rejected and why.</td></tr>
|
||||||
|
<tr><td class="mono">data/</td><td>Default local object store. Parquet datasets, append-log batches, HNSW trial journals, promotion registries, <code>_playbook_memory/state.json</code>, <code>_pathway_memory/state.json</code> (88 traces, 11/11 successful replays, ADR-021), catalog manifests. Plus learning-loop directories: <code>_kb/</code>, <code>_playbook_lessons/</code>, <code>_observer/ops.jsonl</code>, <code>_auditor/kimi_verdicts/</code>. Rebuildable from repo + this dir alone.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -199,20 +204,42 @@ table.plain tr:hover td{background:#0d1117}
|
|||||||
<li>Ollama swaps to the profile's model via <code>keep_alive=0</code>; only one model in VRAM at a time</li>
|
<li>Ollama swaps to the profile's model via <code>keep_alive=0</code>; only one model in VRAM at a time</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>Model matrix (Phase 20)</h3>
|
<h3>Provider fleet — 5 active, 40+ frontier models reachable</h3>
|
||||||
<p>Five tiers declared in <code>config/models.json</code>. Each call site picks the tier appropriate to its purpose — hot-path JSON emitters get fast local, overview/strategic/gatekeeper decisions get thinking models on cloud. Every tier carries <code>context_window</code>, <code>context_budget</code>, and <code>overflow_policy</code>.</p>
|
<p>Declared in <code>config/providers.toml</code> + <code>config/modes.toml</code>. Gateway is an OpenAI-compatible drop-in middleware: any consumer that speaks <code>POST /v1/chat/completions</code> gets routing, audit, cost telemetry, and the full memory substrate behind every call.</p>
|
||||||
<table class="plain">
|
<table class="plain">
|
||||||
<thead><tr><th>Tier</th><th>Purpose</th><th>Primary model</th><th>Frequency</th></tr></thead>
|
<thead><tr><th>Provider</th><th>Reach</th><th>Use case</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>T1 hot</td><td>Per tool call — SQL gen, hybrid_search, propose_done</td><td><code>qwen3.5:latest</code> local, <code>think:false</code></td><td>50-200/scenario</td></tr>
|
<tr><td><code>ollama</code></td><td>localhost:3200 — local sidecar over Ollama</td><td>Hot-path JSON emitters, embeddings, last-resort rescue</td></tr>
|
||||||
<tr><td>T2 review</td><td>Per-step consensus, drift flagging</td><td><code>qwen3:latest</code> local, <code>think:false</code></td><td>5-14/event</td></tr>
|
<tr><td><code>ollama_cloud</code></td><td>ollama.com bearer key — gpt-oss:120b, qwen3-coder:480b, deepseek-v3.1:671b, kimi-k2:1t, mistral-large-3:675b, qwen3.5:397b</td><td>Strong-model reviewer rungs, T3+ overview, scrum master pipeline</td></tr>
|
||||||
<tr><td>T3 overview</td><td>Mid-day checkpoints + cross-day lesson distill</td><td><code>gpt-oss:120b</code> Ollama Cloud, thinking on</td><td>1-3/scenario</td></tr>
|
<tr><td><code>openrouter</code></td><td>openrouter.ai/api/v1 — 343 models incl. Anthropic/Google/OpenAI/MiniMax/Qwen, paid + free tiers</td><td>Paid ladder for observer escalations, free-tier rescue</td></tr>
|
||||||
<tr><td>T4 strategic</td><td>Pattern re-ranking, weekly gap audit</td><td><code>qwen3.5:397b</code> cloud</td><td>1-10/day</td></tr>
|
<tr><td><code>opencode</code></td><td>opencode.ai/zen/v1 — <strong>40 frontier models reachable through ONE sk-* key</strong>: Claude Opus 4.7 / Sonnet / Haiku, GPT-5.5-pro / 5.4 / codex variants, Gemini 3.1-pro, Kimi K2.6, GLM 5.1, DeepSeek, Qwen 3.6+, MiniMax, plus 4 free-tier</td><td>Cross-architecture tie-breakers, auditor cross-lineage (Haiku 4.5 + Opus 4.7), high-context reasoning (Opus on diffs >100k chars)</td></tr>
|
||||||
<tr><td>T5 gatekeeper</td><td>Schema migrations, autotune config changes</td><td><code>kimi-k2-thinking</code> cloud, audit-logged</td><td>1-5/day</td></tr>
|
<tr><td><code>kimi</code></td><td>api.kimi.com/coding/v1 — direct Kimi For Coding</td><td>kimi_architect when ollama_cloud rate-limits; TOS-clean primary path</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p><strong>Key mechanical finding (2026-04-21):</strong> qwen3.5 and qwen3 are <em>thinking</em> models — they burn ~650 tokens of hidden reasoning before emitting the visible response. For hot-path JSON emitters this meant 400-token budgets returned empty strings. Fix: <code>think: false</code> plumbed through sidecar's <code>/generate</code> endpoint; hot path disables thinking (structure matters more than reasoning depth), overseer tiers keep it on. Mistral was dropped entirely after a 0/14 fill rate on complex scenarios (decoder-level malformed-JSON bug, not a prompt issue).</p>
|
|
||||||
<p><strong>Continuation primitive (Phase 21):</strong> <code>generateContinuable()</code> handles output-overflow without <code>max_tokens</code> tourniquets — empty response → geometric backoff retry; truncated-JSON → continue with partial as scratchpad. <code>generateTreeSplit()</code> handles input-overflow via map-reduce with running scratchpad. Both respect <code>assertContextBudget()</code> so silent truncation can't happen.</p>
|
<h3>The 9-rung cloud-first ladder</h3>
|
||||||
|
<p>Defined in <code>tests/real-world/scrum_master_pipeline.ts</code> as <code>const LADDER</code>. Each attempt is evaluated by <code>isAcceptable()</code> = chars ≥ 3800 ∧ not malformed JSON-only. On reject, the next rung sees a learning preamble carrying the prior rejection reason.</p>
|
||||||
|
<pre>1 ollama_cloud / kimi-k2:1t 1T params · flagship
|
||||||
|
2 ollama_cloud / qwen3-coder:480b coding specialist
|
||||||
|
3 ollama_cloud / deepseek-v3.1:671b reasoning
|
||||||
|
4 ollama_cloud / mistral-large-3:675b deep analysis
|
||||||
|
5 ollama_cloud / gpt-oss:120b reliable workhorse
|
||||||
|
6 ollama_cloud / qwen3.5:397b dense final thinker
|
||||||
|
7 openrouter / openai/gpt-oss-120b:free rescue tier
|
||||||
|
8 openrouter / google/gemma-3-27b-it:free fastest rescue
|
||||||
|
9 ollama / qwen3.5:latest last-resort local</pre>
|
||||||
|
|
||||||
|
<h3>N=3 consensus + cross-architecture tie-breaker</h3>
|
||||||
|
<p>Every audit and every consensus-required call fires the primary reviewer N=3 times in parallel (Promise.all — wall-clock = single call). Aggregate votes per claim_idx, majority wins. On a 1-1-1 split, a tie-breaker model with <em>different architecture</em> (qwen3-coder:480b vs primary gpt-oss/kimi) is invoked. Every disagreement, even when majority resolves, writes to <code>data/_kb/audit_discrepancies.jsonl</code>. Closes the cloud-non-determinism gap: <code>temp=0</code> isn't actually deterministic in practice across hours; consensus + cross-architecture tie-break stabilizes verdicts.</p>
|
||||||
|
|
||||||
|
<h3>Auditor cross-lineage (Kimi ↔ Haiku ↔ Opus)</h3>
|
||||||
|
<p>Every push to PR #11 triggers <code>auditor/audit.ts</code> within ~90s. To prevent a single model lineage's blind spots from becoming the system's blind spots, audits alternate between Kimi K2.6 (Moonshot lineage) and Haiku 4.5 (Anthropic lineage) by head SHA. Diffs over 100k chars auto-promote to Claude Opus 4.7 (Anthropic frontier). Per-PR cap of 3 audits with auto-reset on each new head SHA prevents infinite-loop spend. <strong>Latest verdict on c3c9c21:</strong> Haiku 4.5, 24.6s, 100% grounding-verified across 10 findings.</p>
|
||||||
|
|
||||||
|
<h3>Distillation v1.0.0 — the frozen substrate</h3>
|
||||||
|
<p>The substrate the auditor and mode runner sit on is tagged at <code>distillation-v1.0.0</code> / commit <code>e7636f2</code>. <strong>145 unit tests pass · 22/22 acceptance invariants · 16/16 audit-full checks · bit-identical reproducibility verified.</strong> The distillation phase exports clean SFT / RAG / preference samples with a multi-layer contamination firewall (<code>SFT_NEVER</code> constant + scorer category mapping + acceptance fixtures); the auditor consumes the substrate. The frozen tag means: any future "the system regressed" question has a baseline to bisect against, byte-for-byte.</p>
|
||||||
|
|
||||||
|
<h3>Continuation primitive (Phase 21)</h3>
|
||||||
|
<p><code>generateContinuable()</code> handles output-overflow without <code>max_tokens</code> tourniquets — empty response → geometric backoff retry; truncated-JSON → continue with partial as scratchpad. <code>generateTreeSplit()</code> handles input-overflow via map-reduce with running scratchpad. Both respect <code>assertContextBudget()</code> so silent truncation can't happen. Now Rust-native in <code>crates/aibridge/src/continuation.rs</code> (Phase 44).</p>
|
||||||
|
|
||||||
<h3>Per-staffer tool_level (Phase 23)</h3>
|
<h3>Per-staffer tool_level (Phase 23)</h3>
|
||||||
<p>Scenarios can be scoped to a specific coordinator (<code>staffer: {id, name, tenure_months, role, tool_level}</code>). <code>tool_level</code> controls which tiers are available:</p>
|
<p>Scenarios can be scoped to a specific coordinator (<code>staffer: {id, name, tenure_months, role, tool_level}</code>). <code>tool_level</code> controls which tiers are available:</p>
|
||||||
@ -265,6 +292,12 @@ table.plain tr:hover td{background:#0d1117}
|
|||||||
<tr><td>Boost workers based on past success</td><td>No</td><td>Yes (Phase 19 playbook_memory)</td></tr>
|
<tr><td>Boost workers based on past success</td><td>No</td><td>Yes (Phase 19 playbook_memory)</td></tr>
|
||||||
<tr><td>Penalize workers based on past failure</td><td>No</td><td>Yes (<code>/log_failure</code> + <code>0.5<sup>n</sup></code> penalty)</td></tr>
|
<tr><td>Penalize workers based on past failure</td><td>No</td><td>Yes (<code>/log_failure</code> + <code>0.5<sup>n</sup></code> penalty)</td></tr>
|
||||||
<tr><td>Surface traits across past fills</td><td>No</td><td>Yes (<code>/vectors/playbook_memory/patterns</code>)</td></tr>
|
<tr><td>Surface traits across past fills</td><td>No</td><td>Yes (<code>/vectors/playbook_memory/patterns</code>)</td></tr>
|
||||||
|
<tr><td>Per-staffer relevance gradient</td><td>No</td><td>Yes — same query reshapes per coordinator (<code>staffer_id</code> on <code>/intelligence/chat</code>); MARIA'S MEMORY pill labels the playbook context with the active coordinator</td></tr>
|
||||||
|
<tr><td>Triage in one shot — late-worker → backfills + draft SMS</td><td>No</td><td>Yes (<code>/intelligence/chat</code> Route 6 — pulls profile + 5 same-role same-geo backfills sorted by responsiveness + drafts client SMS in ~250ms)</td></tr>
|
||||||
|
<tr><td>Permit → fill plan derivation (forward demand)</td><td>No</td><td>Yes (<code>/intelligence/permit_contracts</code> — Chicago Socrata permit → role / headcount / deadline / fill probability / gross revenue per card)</td></tr>
|
||||||
|
<tr><td>Public-issuer attribution across contractor graph</td><td>No</td><td>Yes (<code>/intelligence/profiler_index</code> — direct + parent + co-permit associated tickers; live Stooq prices)</td></tr>
|
||||||
|
<tr><td>Cross-lineage AI audit on every PR</td><td>No</td><td>Yes (auditor crate — Kimi K2.6 ↔ Haiku 4.5 alternation + Opus 4.7 auto-promote on big diffs)</td></tr>
|
||||||
|
<tr><td>Pathway memory — system-level hot-swap by task fingerprint</td><td>No</td><td>Yes (88 traces, 11/11 successful replays, 100% reuse rate, ADR-021)</td></tr>
|
||||||
<tr><td>Predict staffing demand from external data</td><td>No</td><td>Yes (Chicago permit feed + 30-day rolling forecast)</td></tr>
|
<tr><td>Predict staffing demand from external data</td><td>No</td><td>Yes (Chicago permit feed + 30-day rolling forecast)</td></tr>
|
||||||
<tr><td>Count down to staffing deadline per contract</td><td>No</td><td>Yes (permit issue_date + heuristic timeline)</td></tr>
|
<tr><td>Count down to staffing deadline per contract</td><td>No</td><td>Yes (permit issue_date + heuristic timeline)</td></tr>
|
||||||
<tr><td>Explain why each candidate ranked</td><td>No</td><td>Yes (boost chip + narrative citations + memory pattern)</td></tr>
|
<tr><td>Explain why each candidate ranked</td><td>No</td><td>Yes (boost chip + narrative citations + memory pattern)</td></tr>
|
||||||
@ -278,7 +311,7 @@ table.plain tr:hover td{background:#0d1117}
|
|||||||
<div class="chapter" id="ch6">
|
<div class="chapter" id="ch6">
|
||||||
<div class="num">Chapter 6</div>
|
<div class="num">Chapter 6</div>
|
||||||
<h2>How it gets better over time</h2>
|
<h2>How it gets better over time</h2>
|
||||||
<div class="lede">Compounding learning across seven paths. The first three are automatic background loops. Paths 4-7 landed 2026-04-21 and turn the system into a reinforcement-learning pipeline: outcomes → knowledge base → pathway recommendations → cloud rescue → competence-weighted retrieval → observer analysis. All seven happen without operator intervention.</div>
|
<div class="lede">Compounding learning across ten paths. The first three are automatic background loops. Paths 4-7 (Phase 22-24) added the reinforcement layer: outcomes → KB → recommendations → cloud rescue → competence-weighted retrieval → observer analysis. Paths 7-9 (Phase 25-43, 2026-04-26→27) added the system-level memory layers: pathway memory by task fingerprint (ADR-021), per-staffer hot-swap, and the Construction Activity Signal Engine. All ten happen without operator intervention.</div>
|
||||||
|
|
||||||
<h3>Path 1 — Playbook boost with geo + role prefilter (Phase 19 + refinement)</h3>
|
<h3>Path 1 — Playbook boost with geo + role prefilter (Phase 19 + refinement)</h3>
|
||||||
<p>Every sealed fill is seeded to <code>playbook_memory</code>. The boost fires inside <code>/vectors/hybrid</code> when <code>use_playbook_memory: true</code>. Math, tightened 2026-04-21 after a diagnostic pass found globally-ranked playbooks were missing the SQL-filtered candidate pool entirely:</p>
|
<p>Every sealed fill is seeded to <code>playbook_memory</code>. The boost fires inside <code>/vectors/hybrid</code> when <code>use_playbook_memory: true</code>. Math, tightened 2026-04-21 after a diagnostic pass found globally-ranked playbooks were missing the SQL-filtered candidate pool entirely:</p>
|
||||||
@ -311,7 +344,19 @@ boost[(city, state, name)] = min(Σ per_worker, 0.25)</pre>
|
|||||||
<p>Answers "who handled this" as a first-class matrix-index dimension. Each scenario carries <code>staffer: {id, name, tenure_months, role, tool_level}</code>. After every run, <code>recomputeStafferStats(staffer_id)</code> aggregates their fill_rate, turn efficiency, citation density, rescue rate into a single <code>competence_score</code> (0.45·fill + 0.20·turn_eff + 0.20·cites + 0.15·rescue).</p>
|
<p>Answers "who handled this" as a first-class matrix-index dimension. Each scenario carries <code>staffer: {id, name, tenure_months, role, tool_level}</code>. After every run, <code>recomputeStafferStats(staffer_id)</code> aggregates their fill_rate, turn efficiency, citation density, rescue rate into a single <code>competence_score</code> (0.45·fill + 0.20·turn_eff + 0.20·cites + 0.15·rescue).</p>
|
||||||
<p><code>findNeighbors</code> returns <code>weighted_score = cosine × max_staffer_competence</code> — top-performer playbooks rank above juniors' on similar scenarios. Auto-discovery emerges: running 4 staffers × 3 contracts × 3 rounds surfaced Rachel D. Lewis (Welder Nashville) with 18 endorsements across all 4 staffers, Angela U. Ward (Machine Op Indianapolis) with 19 — reliable-performer labels the system built without human tagging.</p>
|
<p><code>findNeighbors</code> returns <code>weighted_score = cosine × max_staffer_competence</code> — top-performer playbooks rank above juniors' on similar scenarios. Auto-discovery emerges: running 4 staffers × 3 contracts × 3 rounds surfaced Rachel D. Lewis (Welder Nashville) with 18 endorsements across all 4 staffers, Angela U. Ward (Machine Op Indianapolis) with 19 — reliable-performer labels the system built without human tagging.</p>
|
||||||
|
|
||||||
<h3>Path 7 — Observer outcome ingest (Phase 24)</h3>
|
<h3>Path 7 — Pathway memory (ADR-021 — semantic-correctness matrix layer)</h3>
|
||||||
|
<p>Memory at the system layer, not the worker layer. Every accepted scrum review writes a <code>PathwayTrace</code> with the full backtrack: file fingerprint, model used, signal class, KB chunks consulted, observer events, semantic flags (UnitMismatch, TypeConfusion, OffByOne, StaleReference, DeadCode, BoundaryViolation, …), bug fingerprints. A new query that fingerprints to the same trace can hot-swap to the prior result without re-running the 9-rung escalation. Five-factor hot-swap gate: narrow fingerprint match AND audit consensus pass AND replay_count ≥ 3 (probation) AND success_rate ≥ 0.80 AND NOT retired AND vector cosine ≥ 0.90.</p>
|
||||||
|
<p><strong>Live state (verified on this load):</strong> 88 traces · 11 / 11 successful replays · 100% reuse rate · probation gate crossed. Endpoints: <code>/vectors/pathway/insert</code> · <code>/query</code> · <code>/record_replay</code> · <code>/stats</code> · <code>/bug_fingerprints</code>. Spec: <code>docs/DECISIONS.md</code> ADR-021.</p>
|
||||||
|
|
||||||
|
<h3>Path 8 — Per-staffer hot-swap index</h3>
|
||||||
|
<p>Memory scoped to whoever's acting. <code>/intelligence/chat</code> accepts <code>staffer_id</code>; on match, defaults state filter to staffer territory, scopes playbook-pattern geo to staffer's primary city/state, and surfaces <code>response.staffer.name</code> so the UI relabels MEMORY → MARIA'S MEMORY. Same query "forklift operators" returns 167 IL workers as Maria, 89 IN as Devon, 16 WI as Aisha. The corpus stays intact; the relevance gradient is per coordinator; each accumulates fills independently.</p>
|
||||||
|
<p><strong>Roster:</strong> <code>/staffers</code> endpoint reads from <code>STAFFERS</code> in <code>mcp-server/index.ts</code>. Three personas today (Maria/Devon/Aisha); architecture generalizes — every new metro adds territories, not code paths.</p>
|
||||||
|
|
||||||
|
<h3>Path 9 — Construction Activity Signal Engine</h3>
|
||||||
|
<p>Memory at the network layer. Every contractor in the corpus is also a forward indicator on the public equities they touch via three attribution flavors: <code>direct</code> (contractor IS the public issuer — SEC tickers index match), <code>parent</code> (subsidiary of a public parent — curated KNOWN_PARENT_MAP, e.g. Turner → HOC.DE via Hochtief AG), <code>associated</code> (co-permit network — Bob's Electric appears with TARGET CORPORATION 3+ times → inherits TGT). The associated path is the moat: a staffing-permit dataset that maps contractor-to-public-issuer is not commercially available; we synthesize it from the Socrata co-occurrence graph.</p>
|
||||||
|
<p><strong>BAI (Building Activity Index)</strong> = attribution-weighted average day-change across surfaced issuers. <strong>Indexed build value</strong> = total $ of permits attributable to ANY public issuer in scope. <strong>Network depth</strong> = issuers / total attribution edges. Cross-metro replication explicit in the architecture — Chicago is Phase 1; NYC DOB / LA County / Houston BCD / Boston ISD / DC DCRA are all Socrata-shaped, ship as config-only adapters.</p>
|
||||||
|
|
||||||
|
<h3>Path 10 — Observer outcome ingest (Phase 24)</h3>
|
||||||
<p>Observer runs as <code>lakehouse-observer.service</code>, now with an HTTP listener on <code>:3800</code>. Scenarios POST per-event outcomes to <code>/event</code> with full provenance (staffer_id, sig_hash, event_kind, role, city, state, rescue flags). Observer's ERROR_ANALYZER and PLAYBOOK_BUILDER loops consume them alongside MCP-wrapped ops. Persistence switched from the old <code>/ingest/file</code> REPLACE path to an append-only <code>data/_observer/ops.jsonl</code> journal so the trace survives across restarts.</p>
|
<p>Observer runs as <code>lakehouse-observer.service</code>, now with an HTTP listener on <code>:3800</code>. Scenarios POST per-event outcomes to <code>/event</code> with full provenance (staffer_id, sig_hash, event_kind, role, city, state, rescue flags). Observer's ERROR_ANALYZER and PLAYBOOK_BUILDER loops consume them alongside MCP-wrapped ops. Persistence switched from the old <code>/ingest/file</code> REPLACE path to an append-only <code>data/_observer/ops.jsonl</code> journal so the trace survives across restarts.</p>
|
||||||
|
|
||||||
<h3>Input normalizer + unified memory query</h3>
|
<h3>Input normalizer + unified memory query</h3>
|
||||||
@ -399,7 +444,11 @@ boost[(city, state, name)] = min(Σ per_worker, 0.25)</pre>
|
|||||||
<div class="chapter" id="ch9">
|
<div class="chapter" id="ch9">
|
||||||
<div class="num">Chapter 9</div>
|
<div class="num">Chapter 9</div>
|
||||||
<h2>Per-staffer context</h2>
|
<h2>Per-staffer context</h2>
|
||||||
<div class="lede">Twenty staffers don't see the same UI state. Each one's session is shaped by their active profile, their workspaces, their assigned contracts, and their client's blacklists.</div>
|
<div class="lede">Twenty staffers don't see the same UI state. Each one's session is shaped by their identity (the per-staffer hot-swap index — Path 8 in Ch6), their active profile, their workspaces, their assigned contracts, and their client's blacklists.</div>
|
||||||
|
|
||||||
|
<h3>Per-staffer hot-swap index (the recent layer)</h3>
|
||||||
|
<p>Maria runs Chicago. Devon runs Indianapolis. Aisha runs Wisconsin/Michigan. They share one corpus, but search results, recurring-skill patterns, and playbook context all reshape to whoever is acting. <code>/intelligence/chat</code> accepts <code>staffer_id</code>; on match, defaults state filter to the staffer's territory, scopes playbook-pattern geo to their primary city/state, and surfaces <code>response.staffer.name</code> so the UI relabels MEMORY → <em>MARIA'S MEMORY</em>.</p>
|
||||||
|
<p><strong>Verified end-to-end:</strong> same query "forklift operators" returns 167 IL workers as Maria, 89 IN as Devon, 16 WI as Aisha (live numbers; refresh the profiler page to recompute). The corpus stays intact; the relevance gradient is per coordinator. As each accumulates fills, their slice of the playbook compounds independently. <strong>Roster:</strong> <code>/staffers</code> endpoint, declared in <code>STAFFERS</code> in <code>mcp-server/index.ts</code>. Adding a staffer is one append; the architecture is metro-agnostic by construction.</p>
|
||||||
|
|
||||||
<h3>Active profile (Phase 17)</h3>
|
<h3>Active profile (Phase 17)</h3>
|
||||||
<p>Scopes every search. A <code>staffing-recruiter</code> profile bound to <code>workers_500k</code> sees only that dataset. A <code>security-analyst</code> profile bound to <code>threat_intel</code> cannot see worker data. <code>GET /vectors/profile/<id>/audit</code> records every tool invocation by model identity.</p>
|
<p>Scopes every search. A <code>staffing-recruiter</code> profile bound to <code>workers_500k</code> sees only that dataset. A <code>security-analyst</code> profile bound to <code>threat_intel</code> cannot see worker data. <code>GET /vectors/profile/<id>/audit</code> records every tool invocation by model identity.</p>
|
||||||
@ -446,7 +495,7 @@ boost[(city, state, name)] = min(Σ per_worker, 0.25)</pre>
|
|||||||
|
|
||||||
<div class="step"><div class="n">12:30</div><div class="body"><strong>Client pushes 20 new contracts + 1M ATS delta.</strong> Ch7 scale flow fires. Ingest in seconds; embedding refresh kicks off as a background job. Searches continue against old embeddings.</div></div>
|
<div class="step"><div class="n">12:30</div><div class="body"><strong>Client pushes 20 new contracts + 1M ATS delta.</strong> Ch7 scale flow fires. Ingest in seconds; embedding refresh kicks off as a background job. Searches continue against old embeddings.</div></div>
|
||||||
|
|
||||||
<div class="step"><div class="n">14:00</div><div class="body"><strong>Emergency: worker Dave no-showed.</strong> Sarah clicks No-show button on Dave's card → <code>/log_failure</code> → <code>mark_failed</code> records a penalty. Next similar query dampens Dave's boost by 0.5. Sarah continues the refill — the refill excludes Dave and the 2 others already booked for this shift.</div></div>
|
<div class="step"><div class="n">14:00</div><div class="body"><strong>Emergency: worker Dave no-showed.</strong> Sarah types "Dave running late site 4422" into the search box. ~250ms later: triage card with Dave's profile + reliability + responsiveness, draft SMS to client ("dispatching X from local bench, 96% reliability, will confirm arrival"), and 5 same-role same-geo backfills sorted by responsiveness rendered as a green list below. Sarah clicks Copy SMS, pastes to client, clicks Call on the top backfill. <code>/log_failure</code> on Dave records the penalty for the next similar query.</div></div>
|
||||||
|
|
||||||
<div class="step"><div class="n">15:00</div><div class="body"><strong>New embeddings live.</strong> Hot-swap promotion. Searches now see all 1M new profiles. Sarah's noon query re-run would produce different top-5.</div></div>
|
<div class="step"><div class="n">15:00</div><div class="body"><strong>New embeddings live.</strong> Hot-swap promotion. Searches now see all 1M new profiles. Sarah's noon query re-run would produce different top-5.</div></div>
|
||||||
|
|
||||||
@ -468,14 +517,15 @@ boost[(city, state, name)] = min(Σ per_worker, 0.25)</pre>
|
|||||||
|
|
||||||
<h4>Deferred — real architectural work, just not shipped yet</h4>
|
<h4>Deferred — real architectural work, just not shipped yet</h4>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li><strong>BAI persistence + backtesting.</strong> Building Activity Index is computed live per page load. To validate the thesis (permit activity precedes equity moves) we need the daily series saved over months. Architectural support exists (<code>data/_kb/audit_baselines.jsonl</code> append pattern); just hasn't run long enough.</li>
|
||||||
|
<li><strong>NYC DOB adapter.</strong> Architecture is metro-agnostic — Chicago is Phase 1. NYC DOB ships next as a config-only Socrata adapter; LA County, Houston BCD, Boston ISD, DC DCRA queue behind it. Each new metro multiplies network edges without multiplying the codebase.</li>
|
||||||
|
<li><strong>12 awaiting public-data sources for contractor profile.</strong> DOL Wage & Hour, EPA ECHO, MSHA, BBB, PACER civil suits, UCC liens, D&B credit, State licensure, Surety bonds, DOT/FMCSA, State UI claims, DOL RAPIDS apprenticeships. Listed by name on every contractor profile with a one-line "would show:" sample. Each ships as a Socrata-style adapter; engineering scope is concrete.</li>
|
||||||
<li><strong>Rate / margin awareness.</strong> Worker pay expectations vs contract bill rate not modeled. Requires adding <code>pay_rate</code> to workers, <code>bill_rate</code> to contracts, and a filter + warning path. Partially addressed via <code>ContractTerms.budget_per_hour_max</code> passed to T3/rescue prompts, but the match-time filter isn't wired yet.</li>
|
<li><strong>Rate / margin awareness.</strong> Worker pay expectations vs contract bill rate not modeled. Requires adding <code>pay_rate</code> to workers, <code>bill_rate</code> to contracts, and a filter + warning path. Partially addressed via <code>ContractTerms.budget_per_hour_max</code> passed to T3/rescue prompts, but the match-time filter isn't wired yet.</li>
|
||||||
<li><strong>Mem0-style UPDATE / DELETE / NOOP operations on playbooks.</strong> Today <code>/seed</code> only ADDs. Same <code>(operation, date)</code> pair appends a duplicate instead of refining an existing entry. Phase 26 item — cheap to add, moderate payoff.</li>
|
<li><strong>Mem0-style UPDATE / DELETE / NOOP operations on playbooks.</strong> Today <code>/seed</code> only ADDs. Same <code>(operation, date)</code> pair appends a duplicate instead of refining an existing entry. Cheap to add, moderate payoff.</li>
|
||||||
<li><strong>Letta working-memory hot cache.</strong> Every boost query scans all active playbook entries from in-memory state. 1.9K today; cheap. Will bite somewhere north of 100K. LRU for the last-N playbooks or current-sig neighborhood deferred until that ceiling approaches.</li>
|
<li><strong>Letta working-memory hot cache.</strong> Every boost query scans all active playbook entries from in-memory state. ~5K today; cheap. Will bite somewhere north of 100K. Deferred until the ceiling approaches.</li>
|
||||||
<li><strong>Chunking cache (Phase 21 Rust port).</strong> TS primitives <code>generateContinuable</code> + <code>generateTreeSplit</code> are wired, but <code>crates/aibridge/src/{continuation.rs, tree_split.rs}</code> + <code>crates/storaged/src/chunk_cache.rs</code> remain queued. Gateway-side callers currently don't have the same protection against silent truncation that the TS test harness does.</li>
|
|
||||||
<li><strong>Confidence calibration.</strong> Top-K is a rank, not a probability. No calibrated "85% likely to accept" score. Requires outcome-labeled training data.</li>
|
<li><strong>Confidence calibration.</strong> Top-K is a rank, not a probability. No calibrated "85% likely to accept" score. Requires outcome-labeled training data.</li>
|
||||||
<li><strong>Neural re-ranker.</strong> Phase 19 is statistical + semantic (now with geo + role prefilter, Phase 25 retirement). A (query, candidate, outcome)-trained re-ranker is deferred only if the statistical floor plateaus below usable recall — current 14× citation lift on identical inputs suggests it hasn't.</li>
|
<li><strong>SEC name-to-ticker fuzzy precision.</strong> Current matcher requires ≥2 non-stopword overlap; rare false positives still surface (saw FLG attach to a PNC-adjacent contractor once). Tightenable to require an explicit allow-list for production trading use.</li>
|
||||||
<li><strong>Observer → autotune feedback wire.</strong> Phase 24 streams scenario outcomes into <code>data/_observer/ops.jsonl</code>; autotune agent still runs on its own HNSW-trial schedule and hasn't subscribed to the outcome metric stream yet. Phase 26+ item — connects the last loop.</li>
|
<li><strong>Tighter integration of pathway memory + scrum loop.</strong> ADR-021 substrate is shipped (88 traces, 11/11 replays). The hot-swap gate fires correctly; what's deferred is automatic mode-runner short-circuit when a high-confidence pathway match is available before any cloud call burns.</li>
|
||||||
<li><strong>call_log cross-reference.</strong> Infrastructure present; current synthetic candidates table is too small to cross-ref. Fixes when real ATS lands.</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h4>Non-goals — explicitly out of scope</h4>
|
<h4>Non-goals — explicitly out of scope</h4>
|
||||||
@ -496,6 +546,6 @@ boost[(city, state, name)] = min(Σ per_worker, 0.25)</pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">Lakehouse spec · v2 2026-04-21 · Phases 19-25 shipped (playbook boost, model matrix, continuation, KB, staffer competence, observer ingest, validity windows) · maintained from <code>docs/DECISIONS.md</code> · <a href="proof">architecture live-tested</a> · <a href="console">walkthrough</a></div>
|
<div class="footer">Lakehouse spec · v3 2026-04-27 · Phases 19-45 shipped (playbook boost, KB, staffer competence, observer ingest, validity windows, distillation v1.0.0 substrate frozen at e7636f2, gateway as OpenAI-compat drop-in, mode runner, validator + iterate, pathway memory ADR-021, per-staffer hot-swap, Construction Activity Signal Engine) · maintained from <code>docs/DECISIONS.md</code> · <a href="proof">architecture live-tested</a> · <a href="console">walkthrough</a> · <a href="profiler">profiler</a></div>
|
||||||
|
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|||||||
161
scripts/staffing/fetch_face_pool.py
Normal file
161
scripts/staffing/fetch_face_pool.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
fetch_face_pool.py — pull N synthetic headshots from
|
||||||
|
https://thispersondoesnotexist.com/, write to data/headshots/face_NNNN.jpg,
|
||||||
|
optionally tag each with gender via deepface, emit a JSONL manifest.
|
||||||
|
|
||||||
|
Each fetch is a fresh StyleGAN face — no real people. Deterministic per
|
||||||
|
worker mapping happens at serve time (mcp-server hashes the worker key
|
||||||
|
into the pool); this script just builds the pool.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/staffing/fetch_face_pool.py --count 300 --concurrency 3
|
||||||
|
python3 scripts/staffing/fetch_face_pool.py --count 50 --no-gender
|
||||||
|
|
||||||
|
Re-running is idempotent: existing face_NNNN.jpg files are skipped, and
|
||||||
|
the manifest is rewritten from disk state.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
URL = "https://thispersondoesnotexist.com/"
|
||||||
|
UA = "Lakehouse/1.0 (face-pool fetch · synthetic-only · no real-person tracking)"
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_one(idx: int, out_dir: str) -> tuple[int, str, bool, str | None]:
|
||||||
|
"""Returns (idx, basename, cached, error)."""
|
||||||
|
fname = f"face_{idx:04d}.jpg"
|
||||||
|
full = os.path.join(out_dir, fname)
|
||||||
|
if os.path.exists(full) and os.path.getsize(full) > 1024:
|
||||||
|
return idx, fname, True, None
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(URL, headers={"User-Agent": UA})
|
||||||
|
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||||
|
blob = resp.read()
|
||||||
|
if len(blob) < 1024:
|
||||||
|
return idx, fname, False, f"response too small ({len(blob)} bytes)"
|
||||||
|
with open(full, "wb") as f:
|
||||||
|
f.write(blob)
|
||||||
|
return idx, fname, False, None
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
return idx, fname, False, f"urlerror: {e}"
|
||||||
|
except Exception as e:
|
||||||
|
return idx, fname, False, f"{type(e).__name__}: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_tag_gender(records: list[dict], out_dir: str) -> dict[str, int]:
|
||||||
|
"""If deepface is installed, label each record with gender. Returns
|
||||||
|
a count summary; mutates records in place. On import error, returns
|
||||||
|
None and tags every record as unknown."""
|
||||||
|
try:
|
||||||
|
from deepface import DeepFace # type: ignore
|
||||||
|
except Exception as e:
|
||||||
|
print(f" (deepface unavailable: {e}) — pool will mix naturally")
|
||||||
|
for r in records:
|
||||||
|
r["gender"] = None
|
||||||
|
return {"unknown": len(records)}
|
||||||
|
|
||||||
|
print(" tagging gender via deepface (CPU; ~0.5-1s per face)…")
|
||||||
|
counts: dict[str, int] = {}
|
||||||
|
for i, r in enumerate(records):
|
||||||
|
full = os.path.join(out_dir, r["file"])
|
||||||
|
try:
|
||||||
|
ana = DeepFace.analyze(
|
||||||
|
img_path=full,
|
||||||
|
actions=["gender"],
|
||||||
|
enforce_detection=False,
|
||||||
|
silent=True,
|
||||||
|
)
|
||||||
|
if isinstance(ana, list):
|
||||||
|
ana = ana[0] if ana else {}
|
||||||
|
g_raw = (ana.get("dominant_gender") or "").lower().strip()
|
||||||
|
r["gender"] = (
|
||||||
|
"man" if g_raw.startswith("man") else
|
||||||
|
"woman" if g_raw.startswith("woman") else
|
||||||
|
None
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
r["gender"] = None
|
||||||
|
r["gender_error"] = f"{type(e).__name__}: {e}"
|
||||||
|
counts[r["gender"] or "unknown"] = counts.get(r["gender"] or "unknown", 0) + 1
|
||||||
|
if (i + 1) % 25 == 0:
|
||||||
|
print(f" [{i+1}/{len(records)}] {counts}")
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument("--count", type=int, default=300, help="how many faces to maintain in pool")
|
||||||
|
p.add_argument(
|
||||||
|
"--out",
|
||||||
|
default=os.path.join(os.path.dirname(__file__), "..", "..", "data", "headshots"),
|
||||||
|
)
|
||||||
|
p.add_argument("--concurrency", type=int, default=3, help="parallel fetches (be polite)")
|
||||||
|
p.add_argument("--no-gender", action="store_true", help="skip deepface gender tagging")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
out = os.path.realpath(args.out)
|
||||||
|
os.makedirs(out, exist_ok=True)
|
||||||
|
|
||||||
|
print(f"Fetching {args.count} faces → {out}")
|
||||||
|
print(f"Source: {URL} (synthetic StyleGAN — no real people)")
|
||||||
|
|
||||||
|
results: list[dict] = [None] * args.count # type: ignore
|
||||||
|
t0 = time.time()
|
||||||
|
with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as ex:
|
||||||
|
futs = {ex.submit(fetch_one, i, out): i for i in range(args.count)}
|
||||||
|
for done, fut in enumerate(as_completed(futs), 1):
|
||||||
|
idx, fname, cached, err = fut.result()
|
||||||
|
results[idx] = {
|
||||||
|
"id": idx,
|
||||||
|
"file": fname,
|
||||||
|
"cached": cached,
|
||||||
|
"error": err,
|
||||||
|
}
|
||||||
|
if done % 25 == 0 or done == args.count:
|
||||||
|
ok = sum(1 for r in results if r and not r.get("error"))
|
||||||
|
print(f" [{done}/{args.count}] {ok} ok ({time.time()-t0:.1f}s)")
|
||||||
|
|
||||||
|
# Drop slots that errored or are still None (shouldn't happen)
|
||||||
|
records = [r for r in results if r and not r.get("error")]
|
||||||
|
print(f"\nPool ready: {len(records)} faces, {sum(1 for r in records if r['cached'])} from cache")
|
||||||
|
|
||||||
|
if not args.no_gender and records:
|
||||||
|
print("\nGender-tagging pass:")
|
||||||
|
summary = maybe_tag_gender(records, out)
|
||||||
|
print(f" distribution: {summary}")
|
||||||
|
else:
|
||||||
|
for r in records:
|
||||||
|
r["gender"] = None
|
||||||
|
|
||||||
|
# Strip transient flags before persisting
|
||||||
|
for r in records:
|
||||||
|
r.pop("cached", None)
|
||||||
|
r.pop("error", None)
|
||||||
|
|
||||||
|
manifest = os.path.join(out, "manifest.jsonl")
|
||||||
|
with open(manifest, "w") as f:
|
||||||
|
for r in records:
|
||||||
|
f.write(json.dumps(r) + "\n")
|
||||||
|
print(f"\nManifest: {manifest} ({len(records)} entries)")
|
||||||
|
|
||||||
|
# Quick checksum manifest for downstream debugging
|
||||||
|
h = hashlib.sha256()
|
||||||
|
for r in records:
|
||||||
|
h.update(r["file"].encode())
|
||||||
|
h.update(b"|")
|
||||||
|
h.update((r.get("gender") or "?").encode())
|
||||||
|
print(f"Pool fingerprint (sha256): {h.hexdigest()[:16]}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user