@mindrian_os/cli 2.0.0-beta.31 → 2.0.0-beta.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -463,6 +463,15 @@ function renderMarkdown(census) {
463
463
  lines.push('> ' + GENERATED_NOTE + '. This file is rendered by scripts/build-brain-census.cjs. Edit the source lane data, not this file.');
464
464
  lines.push('');
465
465
 
466
+ // Quick task 260911-axz: name the origin and run date in prose near the
467
+ // top, not only in the Census Meta table below, so a reader who lands
468
+ // mid-file cannot mistake a Theo census for the incumbent one. Additive
469
+ // and gated on the Theo shape; the incumbent render carries no such line.
470
+ if ((laneB && laneB.theo_shape) || _isTheoStatsShape(meta.brain_stats)) {
471
+ lines.push('**This census describes the Theo origin `' + _fmtVal(meta.brain_url) + '` as of ' + _fmtVal(meta.census_date) + '.**');
472
+ lines.push('');
473
+ }
474
+
466
475
  // ---- Census Meta ----
467
476
  lines.push('## Census Meta');
468
477
  lines.push('');
@@ -470,7 +479,11 @@ function renderMarkdown(census) {
470
479
  lines.push('| ----- | ----- |');
471
480
  lines.push('| Census date | ' + _fmtVal(meta.census_date) + ' |');
472
481
  lines.push('| Brain URL | ' + _fmtVal(meta.brain_url) + ' |');
473
- if (meta.brain_stats) {
482
+ if (meta.brain_stats && _isTheoStatsShape(meta.brain_stats)) {
483
+ lines.push('| brain_stats nodes | ' + _fmtVal(meta.brain_stats.nodes) + ' |');
484
+ lines.push('| brain_stats relationships | ' + _fmtVal(meta.brain_stats.relationships) + ' |');
485
+ lines.push('| brain_stats labels | ' + _fmtVal(Array.isArray(meta.brain_stats.labels) ? meta.brain_stats.labels.length : null) + ' |');
486
+ } else if (meta.brain_stats) {
474
487
  lines.push('| brain_stats backend | ' + _fmtVal(meta.brain_stats.backend) + ' |');
475
488
  lines.push('| brain_stats totalRecordCount | ' + _fmtVal(meta.brain_stats.totalRecordCount) + ' |');
476
489
  lines.push('| brain_stats relationshipCount | ' + _fmtVal(meta.brain_stats.relationshipCount) + ' |');
@@ -505,6 +518,124 @@ function renderMarkdown(census) {
505
518
  'PENDING - Lane B aggregate Cypher not yet run (admin key not supplied as of ' +
506
519
  _fmtVal(meta.census_date) + '); Lane A below is complete and citable.'
507
520
  );
521
+ } else if (laneB.theo_shape) {
522
+ // Quick task 260911-axz: the Theo lane. Sourced from brain_stats (C1,
523
+ // C5) and brain_schema (C6-equivalent) rather than the whole-graph
524
+ // Cypher those incumbent queries need; C2, C2a-d, C3, C4, C7, C8, C9 are
525
+ // still attempted and any refusal is rendered as a refusal, never a
526
+ // blank section.
527
+ const r = laneB.results || {};
528
+ lines.push('Source: Theo live `brain_stats` + `brain_schema`. Theo\'s read allow-list rejects a whole-graph ' +
529
+ 'AllNodesScan plan (the C1/C5 queries share it), so those two are answered directly from `brain_stats` ' +
530
+ 'instead of Cypher; every other query below was still attempted and any refusal is recorded honestly.');
531
+ lines.push('');
532
+ lines.push('**C1 Framework totals (source: brain_stats.labels, not a Cypher count):** ' + _fmtVal(r.C1 && r.C1.frameworks) + ' Framework nodes.');
533
+ lines.push('');
534
+ if (r.C2 && r.C2.refused) {
535
+ lines.push('**C2/C3 HAS_* structure coverage:** refused by Theo -- ' + _fmtVal(r.C2.error));
536
+ } else if (Array.isArray(r.C2)) {
537
+ lines.push('**C2/C3 HAS_* structure coverage (grouped alternation):**');
538
+ lines.push('');
539
+ lines.push('| Edge type | Edges | Frameworks with edge |');
540
+ lines.push('| --------- | ----- | --------------------- |');
541
+ for (const row of r.C2) {
542
+ lines.push('| ' + _fmtVal(row.t) + ' | ' + _fmtVal(row.edges) + ' | ' + _fmtVal(row.frameworks_with_t) + ' |');
543
+ }
544
+ } else if (r.C2a || r.C2b || r.C2c || r.C2d) {
545
+ lines.push('**C2/C3 HAS_* structure coverage (single-type fallback used, grouped alternation refused):**');
546
+ lines.push('');
547
+ lines.push('| Edge type | Edges | Frameworks with edge |');
548
+ lines.push('| --------- | ----- | --------------------- |');
549
+ for (const key of ['C2a', 'C2b', 'C2c', 'C2d']) {
550
+ const row = r[key];
551
+ if (!row) continue;
552
+ if (row.refused) {
553
+ lines.push('| ' + key + ' | refused | ' + _fmtVal(row.error) + ' |');
554
+ continue;
555
+ }
556
+ const rr = Array.isArray(row) ? row[0] : row;
557
+ if (!rr) continue;
558
+ lines.push('| ' + _fmtVal(rr.t) + ' | ' + _fmtVal(rr.edges) + ' | ' + _fmtVal(rr.frameworks_with_t) + ' |');
559
+ }
560
+ }
561
+ lines.push('');
562
+ if (r.C3 && r.C3.refused) {
563
+ lines.push('C3 distinct frameworks with any structure: refused by Theo -- ' + _fmtVal(r.C3.error));
564
+ } else {
565
+ lines.push('C3 distinct frameworks with any structure: ' + _fmtVal(r.C3 && r.C3.structured_frameworks) + '.');
566
+ }
567
+ lines.push('');
568
+ {
569
+ const c4 = r.C4 || {};
570
+ const c4str = (sub) => {
571
+ const v = c4[sub];
572
+ if (v && typeof v === 'object' && v.refused) return 'refused (' + _fmtVal(v.error) + ')';
573
+ return _fmtVal(v);
574
+ };
575
+ lines.push('**C4 flow edge counts:** FEEDS_INTO=' + c4str('feeds_into') + ', LEADS_TO=' + c4str('leads_to') + ', ALIAS_OF=' + c4str('alias_of') + '.');
576
+ }
577
+ lines.push('');
578
+ lines.push('**C5 label census** (source: `brain_stats.labels`, a per-label count Theo already provides; ' +
579
+ 'the incumbent\'s "counts sum to more than the node count" UNWIND caveat does not apply here and is not repeated):');
580
+ lines.push('');
581
+ lines.push('| Label | Count |');
582
+ lines.push('| ----- | ----- |');
583
+ for (const row of Array.isArray(r.C5) ? r.C5 : []) {
584
+ lines.push('| ' + _fmtVal(row.label) + ' | ' + _fmtVal(row.c) + ' |');
585
+ }
586
+ lines.push('');
587
+ lines.push('**C6 relationship-type census** (source: `brain_schema`; Theo reports relationship TYPE NAMES ' +
588
+ 'without per-type counts, so the count column is n/a - never fabricated, never silently zero):');
589
+ lines.push('');
590
+ if (r.C6 && r.C6.refused) {
591
+ lines.push('refused by Theo -- ' + _fmtVal(r.C6.error));
592
+ } else {
593
+ lines.push('| Relationship type | Count |');
594
+ lines.push('| ------------------ | ----- |');
595
+ for (const t of (r.C6 && r.C6.relationship_types) || []) {
596
+ lines.push('| ' + _fmtVal(t) + ' | n/a |');
597
+ }
598
+ lines.push('');
599
+ lines.push('property_keys count: ' + _fmtVal(r.C6 && r.C6.property_keys_count) + '.');
600
+ }
601
+ lines.push('');
602
+ lines.push('**C7 duplicate-name scan:**');
603
+ lines.push('');
604
+ if (r.C7 && r.C7.refused) {
605
+ lines.push('refused by Theo -- ' + _fmtVal(r.C7.error));
606
+ } else if (!Array.isArray(r.C7) || r.C7.length === 0) {
607
+ lines.push('No duplicate Framework names found.');
608
+ } else {
609
+ lines.push('| Normalized key | Names | Count |');
610
+ lines.push('| -------------- | ----- | ----- |');
611
+ for (const row of r.C7) {
612
+ lines.push('| ' + _fmtVal(row.k) + ' | ' + _fmtVal(Array.isArray(row.names) ? row.names.join(', ') : row.names) + ' | ' + _fmtVal(row.c) + ' |');
613
+ }
614
+ }
615
+ lines.push('');
616
+ lines.push('**C8 named-absence probes (TRIZ/SCAMPER/Five Whys):**');
617
+ lines.push('');
618
+ if (r.C8 && r.C8.refused) {
619
+ lines.push('refused by Theo -- ' + _fmtVal(r.C8.error));
620
+ } else if (!Array.isArray(r.C8) || r.C8.length === 0) {
621
+ lines.push('Zero rows returned - TRIZ, SCAMPER, and Five Whys are absent as Framework nodes.');
622
+ } else {
623
+ lines.push('Rows returned (NOT absent): ' + r.C8.map((row) => row['f.name'] || row.name).join(', '));
624
+ }
625
+ lines.push('');
626
+ lines.push('**C9 JTBD alias enumeration:**');
627
+ lines.push('');
628
+ if (r.C9 && r.C9.refused) {
629
+ lines.push('refused by Theo -- ' + _fmtVal(r.C9.error));
630
+ } else if (!Array.isArray(r.C9) || r.C9.length === 0) {
631
+ lines.push('Zero rows returned.');
632
+ } else {
633
+ lines.push('| Name |');
634
+ lines.push('| ---- |');
635
+ for (const row of r.C9) {
636
+ lines.push('| ' + _fmtVal(row['f.name'] || row.name) + ' |');
637
+ }
638
+ }
508
639
  } else {
509
640
  if (laneB.drift_caveat) {
510
641
  lines.push(
@@ -821,6 +952,159 @@ async function _runLaneB(key) {
821
952
  return { results, usedFallback };
822
953
  }
823
954
 
955
+ // ---------------------------------------------------------------------------
956
+ // _isTheoStatsShape(stats) - the same dual-shape discipline
957
+ // lib/core/doctor/class-m-brain-smoke.cjs's _layer6 already uses: recognize
958
+ // the incumbent's key (totalRecordCount) FIRST implicitly by requiring its
959
+ // absence here, so the incumbent path can never be misclassified as Theo.
960
+ // True when stats is an object carrying a finite `nodes` number and an
961
+ // array `labels`, and no `totalRecordCount` at all (quick task 260911-axz).
962
+ // ---------------------------------------------------------------------------
963
+ function _isTheoStatsShape(stats) {
964
+ return !!stats
965
+ && typeof stats === 'object'
966
+ && typeof stats.nodes === 'number'
967
+ && Number.isFinite(stats.nodes)
968
+ && Array.isArray(stats.labels)
969
+ && !('totalRecordCount' in stats);
970
+ }
971
+
972
+ // ---------------------------------------------------------------------------
973
+ // _looksLikePlanRejection(result) - Theo's read allow-list refusal does NOT
974
+ // surface as an HTTP error or a JSON-RPC `error` field (both of which
975
+ // brainCall() already turns into `{ ok: false, bodyText }`): it surfaces as
976
+ // an ok:true tool result whose content is the plain-text string
977
+ // "PLAN_REJECTED: plan operator `X` is not on the read allow-list", which
978
+ // brainCall()'s JSON.parse failure wraps as `{ text: <that string> }`
979
+ // (found live, quick task 260911-axz, running C2/C3 against Theo -- both
980
+ // use `count(DISTINCT f)`, which Theo's allow-list rejects). Without this
981
+ // check, a caller reading `r.ok` alone would treat a refusal as a success
982
+ // and either render a blank section or, for C4, silently drop every field
983
+ // (`JSON.stringify` omits `undefined`-valued keys), which is exactly the
984
+ // "blank section" and silent-loss failure this generator exists to avoid.
985
+ // ---------------------------------------------------------------------------
986
+ function _looksLikePlanRejection(result) {
987
+ return !!result && typeof result === 'object' && typeof result.text === 'string'
988
+ && /PLAN_REJECTED|not on the (read )?allow-?list/i.test(result.text);
989
+ }
990
+
991
+ // ---------------------------------------------------------------------------
992
+ // _theoQueryOutcome(r) - normalizes a brainCall() outcome into either the
993
+ // raw result or a `{ refused: true, error }` object, covering BOTH refusal
994
+ // shapes Theo can hand back: an HTTP/transport failure (r.ok === false) and
995
+ // the soft PLAN_REJECTED text payload above (r.ok === true). Every call site
996
+ // in _runLaneBTheo routes through this one function so a refusal is never
997
+ // classified two different ways in two different places.
998
+ // ---------------------------------------------------------------------------
999
+ function _theoQueryOutcome(r) {
1000
+ if (!r.ok) return { refused: true, error: r.bodyText };
1001
+ if (_looksLikePlanRejection(r.result)) return { refused: true, error: r.result.text };
1002
+ return r.result;
1003
+ }
1004
+
1005
+ function _isRefused(value) {
1006
+ return !!value && typeof value === 'object' && value.refused === true;
1007
+ }
1008
+
1009
+ // ---------------------------------------------------------------------------
1010
+ // _runLaneBTheo(key, brainStats) - Theo lane B (quick task 260911-axz).
1011
+ // Additive only: never edits CENSUS_QUERIES or the incumbent _runLaneB
1012
+ // above. Sources C1 and C5 directly from the already-fetched brainStats
1013
+ // (Theo's read allow-list rejects the whole-graph AllNodesScan plan both of
1014
+ // those Cypher queries share), sources C6 from brain_schema (type NAMES
1015
+ // without per-type counts -- honestly recorded as n/a, never fabricated or
1016
+ // silently zero), and STILL ATTEMPTS C2, C2a-d, C3, C4, C7, C8, C9 through
1017
+ // brainCall('brain_query', ...), recording { refused: true, error } for any
1018
+ // id Theo refuses (either refusal shape, via _theoQueryOutcome) instead of
1019
+ // throwing (tests/test-246-census-guard.cjs pins all 13 query ids as Part 8
1020
+ // allow-classified; nothing here removes or rewords a query, only how a
1021
+ // refusal is recorded).
1022
+ // ---------------------------------------------------------------------------
1023
+ async function _runLaneBTheo(key, brainStats) {
1024
+ const results = {};
1025
+
1026
+ // C1: Framework total from brain_stats.labels, not a Cypher count.
1027
+ const frameworkEntry = (brainStats.labels || []).find((l) => l && l.label === 'Framework');
1028
+ results.C1 = {
1029
+ frameworks: frameworkEntry ? frameworkEntry.count : null,
1030
+ source: 'brain_stats.labels (not a Cypher count; Theo refuses the whole-graph plan C1 shares)',
1031
+ };
1032
+
1033
+ // C5-equivalent: brain_stats.labels IS already a per-label census; no
1034
+ // UNWIND is needed or attempted.
1035
+ results.C5 = (brainStats.labels || []).map((l) => ({ label: l && l.label, c: l && l.count }));
1036
+
1037
+ // C6-equivalent: brain_schema's relationship TYPE NAMES, no per-type
1038
+ // counts, plus a property_keys count.
1039
+ const schemaRes = await brainCall('brain_schema', {}, key);
1040
+ const schemaOutcome = _theoQueryOutcome(schemaRes);
1041
+ if (_isRefused(schemaOutcome)) {
1042
+ results.C6 = schemaOutcome;
1043
+ } else {
1044
+ const schemaResult = schemaOutcome || {};
1045
+ results.C6 = {
1046
+ relationship_types: Array.isArray(schemaResult.relationship_types) ? schemaResult.relationship_types : [],
1047
+ property_keys_count: Array.isArray(schemaResult.property_keys) ? schemaResult.property_keys.length : null,
1048
+ };
1049
+ }
1050
+
1051
+ const c2 = CENSUS_QUERIES.find((q) => q.id === 'C2');
1052
+ const r2 = await brainCall('brain_query', { cypher: c2.cypher }, key);
1053
+ const c2Outcome = _theoQueryOutcome(r2);
1054
+ results.C2 = c2Outcome;
1055
+ if (_isRefused(c2Outcome)) {
1056
+ for (const subId of ['C2a', 'C2b', 'C2c', 'C2d']) {
1057
+ const q = CENSUS_QUERIES.find((qq) => qq.id === subId);
1058
+ const r = await brainCall('brain_query', { cypher: q.cypher }, key);
1059
+ results[subId] = _theoQueryOutcome(r);
1060
+ }
1061
+ }
1062
+
1063
+ const c3 = CENSUS_QUERIES.find((q) => q.id === 'C3');
1064
+ const r3 = await brainCall('brain_query', { cypher: c3.cypher }, key);
1065
+ const c3Outcome = _theoQueryOutcome(r3);
1066
+ results.C3 = _isRefused(c3Outcome) ? c3Outcome : (Array.isArray(c3Outcome) ? c3Outcome[0] : c3Outcome);
1067
+
1068
+ const c4entries = CENSUS_QUERIES.filter((q) => q.id === 'C4');
1069
+ const c4merged = {};
1070
+ for (const q of c4entries) {
1071
+ const r = await brainCall('brain_query', { cypher: q.cypher }, key);
1072
+ const outcome = _theoQueryOutcome(r);
1073
+ if (_isRefused(outcome)) {
1074
+ c4merged[q.sub] = outcome;
1075
+ continue;
1076
+ }
1077
+ const row = Array.isArray(outcome) ? outcome[0] : outcome;
1078
+ c4merged[q.sub] = row ? (q.sub in row ? row[q.sub] : null) : null;
1079
+ }
1080
+ results.C4 = c4merged;
1081
+
1082
+ for (const id of ['C7', 'C8', 'C9']) {
1083
+ const q = CENSUS_QUERIES.find((qq) => qq.id === id);
1084
+ const r = await brainCall('brain_query', { cypher: q.cypher }, key);
1085
+ results[id] = _theoQueryOutcome(r);
1086
+ }
1087
+
1088
+ return { results, usedFallback: false, theo_shape: true };
1089
+ }
1090
+
1091
+ // ---------------------------------------------------------------------------
1092
+ // _isColdStartLikeFailure(callResult) - true when a brainCall() result looks
1093
+ // like Theo has not woken up yet (Render cold start) rather than a real,
1094
+ // persistent outage: a fetch failure (httpStatus 0), a client-side timeout
1095
+ // (errorKind 'timeout'), or a 502/503 from the edge. Quick task 260911-axz's
1096
+ // cold-start-one-retry rule (--lane-a) keys on this predicate so a genuine
1097
+ // outage (still failing after the 30s warm-up) is never mistaken for a
1098
+ // cold start and retried forever.
1099
+ // ---------------------------------------------------------------------------
1100
+ function _isColdStartLikeFailure(callResult) {
1101
+ if (!callResult || callResult.ok) return false;
1102
+ if (callResult.errorKind === 'timeout') return true;
1103
+ if (callResult.httpStatus === 0) return true;
1104
+ if (callResult.httpStatus === 502 || callResult.httpStatus === 503) return true;
1105
+ return false;
1106
+ }
1107
+
824
1108
  // ---------------------------------------------------------------------------
825
1109
  // main() - four-mode CLI.
826
1110
  // ---------------------------------------------------------------------------
@@ -854,6 +1138,26 @@ async function main() {
854
1138
  console.error('Brain key unavailable: ' + r.reason);
855
1139
  process.exit(1);
856
1140
  }
1141
+
1142
+ // Cold start, one retry (quick task 260911-axz): Theo runs on Render and
1143
+ // sleeps. A reachability probe up front, separate from _runLaneA's own
1144
+ // per-framework loop, lets a cold-start failure retry ONCE after a 30s
1145
+ // warm-up window before any real probing begins. Only a SECOND failure
1146
+ // after the warm-up concludes a real outage; this generator then stops
1147
+ // and reports rather than writing a partial artifact.
1148
+ let reachProbe = await brainCall('brain_stats', {}, r.key);
1149
+ if (_isColdStartLikeFailure(reachProbe)) {
1150
+ console.error('[census] Theo unreachable on first attempt (' + (reachProbe.httpStatus || 'no status')
1151
+ + ' ' + reachProbe.bodyText + '); this may be a Render cold start. Waiting 30s and retrying ONCE.');
1152
+ await new Promise((resolve) => setTimeout(resolve, 30000));
1153
+ reachProbe = await brainCall('brain_stats', {}, r.key);
1154
+ if (_isColdStartLikeFailure(reachProbe)) {
1155
+ console.error('[census] Theo still unreachable after the 30s retry: ' + (reachProbe.httpStatus || 'no status')
1156
+ + ' ' + reachProbe.bodyText + '. Concluding a real outage, not a cold start. Refusing to write a partial artifact.');
1157
+ process.exit(1);
1158
+ }
1159
+ }
1160
+
857
1161
  const scanned = scanMethodologyCommands();
858
1162
  const laneAResult = await _runLaneA(r.key);
859
1163
  const census = loadExisting();
@@ -888,18 +1192,31 @@ async function main() {
888
1192
  console.error('Brain key unavailable: ' + r.reason);
889
1193
  process.exit(1);
890
1194
  }
891
- const laneB = await _runLaneB(r.key);
1195
+ // Shape detection (quick task 260911-axz): a brain_stats call selects
1196
+ // the Theo lane (_runLaneBTheo) when its shape matches; otherwise
1197
+ // _runLaneB runs exactly as today, unedited. Recognizing the
1198
+ // incumbent's shape first (via the absence check inside
1199
+ // _isTheoStatsShape) is the proof of inertness: the incumbent path
1200
+ // cannot change.
1201
+ const statsProbe = await brainCall('brain_stats', {}, r.key);
1202
+ const brainStats = statsProbe.ok ? statsProbe.result : null;
1203
+ const isTheoShape = _isTheoStatsShape(brainStats);
1204
+ const laneB = isTheoShape ? await _runLaneBTheo(r.key, brainStats) : await _runLaneB(r.key);
1205
+ const laneBSource = isTheoShape
1206
+ ? 'theo-live brain_stats + brain_schema (whole-graph Cypher refused by Theo read allow-list)'
1207
+ : 'render-live';
892
1208
  const census = loadExisting();
893
1209
  census.lane_b = {
894
1210
  results: laneB.results,
895
- lane_b_source: 'render-live',
1211
+ lane_b_source: laneBSource,
896
1212
  key_tier_lane_b: 'admin',
897
1213
  drift_caveat: false,
898
1214
  used_c2_fallback: laneB.usedFallback,
899
1215
  };
1216
+ if (isTheoShape) census.lane_b.theo_shape = true;
900
1217
  census.meta = Object.assign({}, census.meta, {
901
1218
  key_tier_lane_b: 'admin',
902
- lane_b_source: 'render-live',
1219
+ lane_b_source: laneBSource,
903
1220
  });
904
1221
  writeOut(census);
905
1222
  console.log('Lane B complete: results for ' + Object.keys(laneB.results).join(', '));