@azure-id/orc 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +46 -46
  3. package/bin/cli.js +5918 -400
  4. package/bin/mockrun-catalog.js +4 -0
  5. package/bin/verify-contracts.js +4475 -4410
  6. package/bin/verify-package.js +21 -2
  7. package/bin/webui/api.js +108 -8
  8. package/bin/webui/app.html +9 -0
  9. package/bin/webui/css/06-responsive.css +14 -0
  10. package/bin/webui/css/panels/test.css +132 -0
  11. package/bin/webui/fixtures/index.js +23 -1
  12. package/bin/webui/fixtures/test.js +547 -0
  13. package/bin/webui/i18n/en/common.json +1 -0
  14. package/bin/webui/i18n/en/nav.json +1 -0
  15. package/bin/webui/i18n/en/overview.json +4 -0
  16. package/bin/webui/i18n/en/test.json +141 -0
  17. package/bin/webui/i18n/id/common.json +1 -0
  18. package/bin/webui/i18n/id/nav.json +1 -0
  19. package/bin/webui/i18n/id/overview.json +4 -0
  20. package/bin/webui/i18n/id/test.json +141 -0
  21. package/bin/webui/js/01-i18n.js +1 -0
  22. package/bin/webui/js/panels/challenge.js +8 -3
  23. package/bin/webui/js/panels/overview.js +10 -0
  24. package/bin/webui/js/panels/test.js +733 -0
  25. package/bin/webui/serve.js +572 -557
  26. package/mock-run/orc-test.md +327 -0
  27. package/package.json +1 -1
  28. package/templates/agents/MODEL-MAPPING.md +2 -0
  29. package/templates/agents/orc-test-designer-opus-5-high.md +158 -0
  30. package/templates/agents/orc-test-interpreter-opus-5-low.md +130 -0
  31. package/templates/commands/orc-test.md +66 -0
  32. package/templates/skills/_shared/extra-dispatch.md +1346 -1337
  33. package/templates/skills/_shared/live-target.md +188 -0
  34. package/templates/skills/_shared/phases/trace.md +2 -2
  35. package/templates/skills/_shared/wait.md +241 -240
  36. package/templates/skills/orc-test/SKILL.md +233 -0
@@ -52,6 +52,7 @@ const required = [
52
52
  "bin/webui/css/panels/challenge.css",
53
53
  "bin/webui/css/panels/docs.css",
54
54
  "bin/webui/css/panels/extra.css",
55
+ "bin/webui/css/panels/test.css",
55
56
  "bin/webui/css/panels/experiment.css",
56
57
  // The client modules (v0.48.1). The <script> order in app.html is the load
57
58
  // order and the numeric prefix is that order; 99-boot.js must be last.
@@ -82,6 +83,7 @@ const required = [
82
83
  "bin/webui/js/panels/challenge.js",
83
84
  "bin/webui/js/panels/docs.js",
84
85
  "bin/webui/js/panels/extra.js",
86
+ "bin/webui/js/panels/test.js",
85
87
  "bin/webui/js/90-tour.js",
86
88
  "bin/webui/js/91-shortcuts.js",
87
89
  "bin/webui/js/99-boot.js",
@@ -106,6 +108,7 @@ const required = [
106
108
  "bin/webui/fixtures/crosslink.js",
107
109
  "bin/webui/fixtures/mockrun.js",
108
110
  "bin/webui/fixtures/extra.js",
111
+ "bin/webui/fixtures/test.js",
109
112
  // The string tables (v0.43.6). Named here for the same reason: English is the
110
113
  // FALLBACK table every other language falls back to, so a publish that drops
111
114
  // it renders raw dotted keys on every panel, in every language.
@@ -135,6 +138,7 @@ const required = [
135
138
  "bin/webui/i18n/en/challenge.json",
136
139
  "bin/webui/i18n/en/docs.json",
137
140
  "bin/webui/i18n/en/extra.json",
141
+ "bin/webui/i18n/en/test.json",
138
142
  "bin/webui/i18n/en/experiment.json",
139
143
  "bin/webui/i18n/en/tour.json",
140
144
  "bin/webui/i18n/id/common.json",
@@ -159,6 +163,7 @@ const required = [
159
163
  "bin/webui/i18n/id/challenge.json",
160
164
  "bin/webui/i18n/id/docs.json",
161
165
  "bin/webui/i18n/id/extra.json",
166
+ "bin/webui/i18n/id/test.json",
162
167
  "bin/webui/i18n/id/experiment.json",
163
168
  "bin/webui/i18n/id/tour.json",
164
169
  // The mocked runs (v0.46.x): the catalogue module plus the folder it reads.
@@ -449,6 +454,15 @@ const required = [
449
454
  // lint. Both are already Opus 5, so `opus5_only` adds no twin for either.
450
455
  "templates/agents/orc-doc-writer-opus-5-med.md",
451
456
  "templates/agents/orc-doc-checker-opus-5-low.md",
457
+ // v1.5.0 - /orc-test's two agents, and they are two INSTRUMENTS rather than
458
+ // two tiers of one. The designer is `high` because the cheap answer to
459
+ // "design some test cases" is the list the CLI already derived for free; the
460
+ // interpreter is `low` because a harder-thinking interpreter reasons its way
461
+ // to why a leaked stack trace is probably fine in staging, which is exactly
462
+ // the gap it exists to find. Nothing may ever upgrade the interpreter. Both
463
+ // are claude-opus-5, so `opus5_only` adds NO pair and the floor moves by two.
464
+ "templates/agents/orc-test-designer-opus-5-high.md",
465
+ "templates/agents/orc-test-interpreter-opus-5-low.md",
452
466
  // v1.2.1 — the status line explained for the person reading it, in
453
467
  // Simplified Technical English. It ships INTO .claude/hooks/ next to the hook
454
468
  // it describes, because that is where somebody looking at a segment they do
@@ -538,8 +552,13 @@ const agentCount = walkCount(path.join(ROOT, "templates/agents"), ".md");
538
552
  // capability from a lane. Fable 5 survives as a SESSION model (the effort guard
539
553
  // and the statusline still clear it at medium) — that is a different question,
540
554
  // and this wave does not touch it.
541
- if (skillCount < 38) missing.push(`templates/skills (expected >=38 SKILL.md, found ${skillCount})`);
542
- if (agentCount < 46) missing.push(`templates/agents (expected >=46 .md, found ${agentCount})`);
555
+ // v1.5.0: +1 skill (orc-test) and +2 agents - the designer and the interpreter.
556
+ // The lane's recon (T2's `unresolved[]`, T4's flow trace) is dispatched AD-HOC
557
+ // by model+effort and earns no agent, the v0.38.0/v0.45.0 precedent, and that
558
+ // limit is stated in the skill: no `SPAWN` line, so `/orc-retro` cannot
559
+ // aggregate it.
560
+ if (skillCount < 39) missing.push(`templates/skills (expected >=39 SKILL.md, found ${skillCount})`);
561
+ if (agentCount < 48) missing.push(`templates/agents (expected >=48 .md, found ${agentCount})`);
543
562
 
544
563
  // B4 — encoding/mojibake guard. The OneDrive corruption rule becomes a gate:
545
564
  // scan every shipped text file for the U+FFFD replacement char (invalid UTF-8
package/bin/webui/api.js CHANGED
@@ -24,6 +24,7 @@ const { spawn, spawnSync } = require("child_process");
24
24
  const fs = require("fs");
25
25
  const os = require("os");
26
26
  const path = require("path");
27
+ const zlib = require("zlib");
27
28
  const fixtures = require("./fixtures/index.js");
28
29
 
29
30
  const CLI = path.join(__dirname, "..", "cli.js");
@@ -411,6 +412,21 @@ const READS = {
411
412
  "/api/extra/demotion": (q) => ["extra", "demotion", ...(q.run ? [String(q.run)] : [])],
412
413
  "/api/extra/reconcile": (q) => ["extra", "reconcile", String(q.task || "")],
413
414
  "/api/extra/journal/prune/preview": () => ["extra", "journal", "prune", "--dry-run"],
415
+ // v1.5.0 — /orc-test, the lane that RUNS the test. THREE reads, and every one
416
+ // of them is a READ in the strict sense this lane needs: none re-scans the
417
+ // repository, none probes the target, and none writes the ledger. Opening a
418
+ // page must never be a measurement — a measurement nobody asked for is
419
+ // traffic nobody authorized.
420
+ //
421
+ // `orc test show` is the whole computed view (`--json is not a summary`), so
422
+ // the surface, the case matrix, the closed OWASP set, the runs and the
423
+ // findings all arrive in ONE object the panel renders and derives nothing
424
+ // from: not a state word, not a severity, not an OWASP id, not the tier.
425
+ // `status` 0 always, `show` 0 / 2 no run, `ui tools` 0 ready / 1 not ready /
426
+ // 2 the driver is `none` — and 2 there is a SETTING, not a missing tool.
427
+ "/api/test": () => ["test", "status"],
428
+ "/api/test/one": (q) => ["test", "show", String(q.slug || "")],
429
+ "/api/test/ui": () => ["test", "ui", "tools"],
414
430
  "/api/patterns": () => ["pattern", "status"],
415
431
  "/api/gotchas": () => ["gotcha", "list"],
416
432
  "/api/stats": (q) => (q.since ? ["stats", "--since", String(q.since)] : ["stats"]),
@@ -671,6 +687,24 @@ const WRITES = {
671
687
  // interactive prompt collects, and every rejection the user sees is the
672
688
  // CLI's own validator speaking — there is no second idea of a valid slug,
673
689
  // a valid kind or a valid edge target anywhere in this panel.
690
+ // v1.5.0 — the THREE /orc-test mutations this panel may make, and NONE of
691
+ // them sends a request to the system under test. `orc test run` is where the
692
+ // traffic is, and it is a COPY-ABLE COMMAND here and never a button: a page
693
+ // that can start a scan against a live host is a page that can start one by
694
+ // accident.
695
+ //
696
+ // · `surface` is the FREE PASS — it reads the repository and the target's
697
+ // own spec document, and it is what every later step is derived from.
698
+ // · `env` is a HEALTH PROBE, the /orc-test equivalent of `orc extra ping`:
699
+ // it asks the target whether it is up. It never starts, stops or fixes
700
+ // anything, and on a REMOTE target it refuses BY NAME.
701
+ // · `report` RENDERS the ledger into REPORT.md and derives nothing.
702
+ //
703
+ // Every one costs zero model tokens, which is the line this panel does not
704
+ // cross. Nothing here dispatches an agent.
705
+ "/api/test/surface": (b) => ["test", "surface", String(b.slug)],
706
+ "/api/test/env": (b) => ["test", "env", String(b.slug)],
707
+ "/api/test/report": (b) => ["test", "report", String(b.slug)],
674
708
  "/api/crosslink/add": (b) => {
675
709
  const argv = ["crosslink", "add", String(b.name), String(b.repo_path), "--kinds", String(b.kinds)];
676
710
  if (b.direction) argv.push("--direction", String(b.direction));
@@ -875,14 +909,80 @@ function launchClaude(ctx) {
875
909
 
876
910
  // ── request handling ────────────────────────────────────────────────────────
877
911
 
912
+ // A RESPONSE OVER 64 KiB IS A HAZARD ON WINDOWS LOOPBACK (v1.5.0).
913
+ //
914
+ // Measured, and NOT an ORC bug: a twenty-line plain `http.createServer` on this
915
+ // platform loses the tail of a response larger than the 64 KiB socket buffer
916
+ // roughly one time in six, after a handful of short-lived connections. The
917
+ // client receives ~65,077 of 69,177 bytes, the server's `finish` fires and
918
+ // `writableFinished` is true, and nineteen seconds later the client gets
919
+ // ECONNRESET with nothing in any log to say why. Under 60,000 bytes it never
920
+ // happened in any run.
921
+ //
922
+ // `/api/config` was 64,934 bytes — six hundred short of that line — so the
923
+ // Settings tab was one config key away from failing with no message, and it
924
+ // crossed on the release that added five. Two things answer it, and neither is
925
+ // a size limit on what this API may say (`--json is not a summary`):
926
+ //
927
+ // 1. DECLARE THE LENGTH. Without `content-length` Node falls back to chunked
928
+ // encoding, which is a worse shape for a body of known size and gives the
929
+ // client no way to tell a truncated read from a complete one.
930
+ // 2. COMPRESS IT WHEN THE CLIENT ASKS. Every browser sends
931
+ // `accept-encoding: gzip`, and the panel IS a browser — gzip takes that
932
+ // 69 KB answer to about 8 KB, far below the cliff. It is skipped for a
933
+ // small body, where the CPU is not worth it, and skipped entirely for a
934
+ // client that did not ask.
935
+ const GZIP_MIN_BYTES = 32 * 1024;
936
+ // `gzip` as a WHOLE token in the header's comma list, so `x-gzip-not-really`
937
+ // never matches. Written out rather than with a regex word boundary, which is
938
+ // not one beside a hyphen.
939
+ const GZIP_ACCEPTED = /(^|,)\s*gzip\s*(;|,|$)/;
940
+
941
+ // ONE implementation, and serve.js uses it for STATIC files too (v1.5.0). The
942
+ // 64 KiB cliff is a property of the SOCKET, not of the content type: it does not
943
+ // care whether the bytes are an API answer or a panel script. Leaving the static
944
+ // path uncompressed left the two largest files in the app — `extra.js` at 138 KB
945
+ // and `hookui.js` at 76 KB — riding the exact path this was written to fix. It
946
+ // showed up as an intermittent ECONNRESET in the asset-walk test, and it would
947
+ // show up in a real browser as a panel script that stops mid-function.
948
+ //
949
+ // Returns the bytes to send plus the headers that describe them. A caller that
950
+ // forgets to declare the length is the other half of the same bug, so the length
951
+ // is set HERE rather than left to each call site.
952
+ function encodeBody(raw, acceptEncoding) {
953
+ const headers = {};
954
+ let body = raw;
955
+ if (raw.length >= GZIP_MIN_BYTES && GZIP_ACCEPTED.test(String(acceptEncoding || ""))) {
956
+ try {
957
+ body = zlib.gzipSync(raw);
958
+ headers["content-encoding"] = "gzip";
959
+ // A cache keyed on the URL alone would hand a gzip body to a client that
960
+ // cannot read one. Nothing caches on loopback today; saying so costs one
961
+ // header and removes the whole class.
962
+ headers["vary"] = "accept-encoding";
963
+ } catch (_) {
964
+ body = raw;
965
+ }
966
+ }
967
+ headers["content-length"] = String(body.length);
968
+ return { body, headers };
969
+ }
970
+
878
971
  function json(res, status, obj) {
879
- const body = JSON.stringify(obj);
880
- res.writeHead(status, {
881
- "content-type": "application/json; charset=utf-8",
882
- "cache-control": "no-store",
883
- // Belt and braces on top of the loopback + token checks in serve.js.
884
- "x-content-type-options": "nosniff",
885
- });
972
+ const raw = Buffer.from(JSON.stringify(obj), "utf8");
973
+ const { body, headers } = encodeBody(raw, res.req && res.req.headers && res.req.headers["accept-encoding"]);
974
+ res.writeHead(
975
+ status,
976
+ Object.assign(
977
+ {
978
+ "content-type": "application/json; charset=utf-8",
979
+ "cache-control": "no-store",
980
+ // Belt and braces on top of the loopback + token checks in serve.js.
981
+ "x-content-type-options": "nosniff",
982
+ },
983
+ headers
984
+ )
985
+ );
886
986
  res.end(body);
887
987
  }
888
988
 
@@ -1285,4 +1385,4 @@ function isDirtyTree(ctx) {
1285
1385
  }
1286
1386
  }
1287
1387
 
1288
- module.exports = { handleApi, clearCache, READS, WRITES, MAINTENANCE };
1388
+ module.exports = { handleApi, clearCache, encodeBody, READS, WRITES, MAINTENANCE };
@@ -42,6 +42,7 @@
42
42
  <link rel="stylesheet" href="css/panels/challenge.css">
43
43
  <link rel="stylesheet" href="css/panels/docs.css">
44
44
  <link rel="stylesheet" href="css/panels/extra.css">
45
+ <link rel="stylesheet" href="css/panels/test.css">
45
46
  <link rel="stylesheet" href="css/06-responsive.css">
46
47
  <link rel="stylesheet" href="css/04-motion.css">
47
48
  </head>
@@ -97,6 +98,13 @@
97
98
  -->
98
99
  <li><a href="#/docs" data-panel="docs" data-idx="d" data-i18n="nav.docs">Docs</a></li>
99
100
  <li><a href="#/challenge" data-panel="challenge" data-idx="c" data-i18n="nav.challenge">Challenge</a></li>
101
+ <!--
102
+ v1.5.0. Test sits directly under Challenge because it is the same kind
103
+ of thing one step further on: Challenge grades what you WROTE, this
104
+ reports what a running system actually DID. Both stop and hand back
105
+ rather than fixing what they measured.
106
+ -->
107
+ <li><a href="#/test" data-panel="test" data-idx="t" data-i18n="nav.test">Test</a></li>
100
108
  <li><a href="#/pact" data-panel="pact" data-idx="p" data-i18n="nav.pact">Promises</a></li>
101
109
  <li><a href="#/boundary" data-panel="boundary" data-idx="b" data-i18n="nav.boundary">Boundary</a></li>
102
110
  <li><a href="#/wait" data-panel="wait" data-idx="w" data-i18n="nav.wait">Wait</a></li>
@@ -206,6 +214,7 @@
206
214
  <script src="js/panels/extra.js"></script>
207
215
  <script src="js/panels/wait.js"></script>
208
216
  <script src="js/panels/hookui.js"></script>
217
+ <script src="js/panels/test.js"></script>
209
218
  <script src="js/90-tour.js"></script>
210
219
  <script src="js/91-shortcuts.js"></script>
211
220
  <script src="js/99-boot.js"></script>
@@ -76,6 +76,20 @@
76
76
  band template here would put the target under the caret. */
77
77
  .ex-slot-head { grid-template-columns: minmax(0, 40%) minmax(0, 1fr) 16px; }
78
78
  .ex-slot-when { grid-column: 1 / -1; }
79
+ /* v1.5.0 — /orc-test. Every grid here DECLARES its columns, so every one of
80
+ them collapses explicitly rather than inheriting a template it no longer
81
+ has the width for. The route, the case row and the OWASP row all keep the
82
+ SAME child count in every state (that is the `.ex-tool` lesson), so the
83
+ collapse is a template change and never a hidden child. */
84
+ .ts-route { grid-template-columns: 56px minmax(0, 1fr); }
85
+ .ts-route > .chip { grid-column: 2; justify-self: start; }
86
+ .ts-where { grid-column: 1 / -1; }
87
+ .ts-case { grid-template-columns: 56px minmax(0, 1fr); }
88
+ .ts-case > .chip { grid-column: 2; justify-self: start; }
89
+ .ts-case-mid { grid-column: 1 / -1; }
90
+ .ts-case-tail { grid-column: 1 / -1; justify-content: flex-start; }
91
+ .ts-owasp { grid-template-columns: 56px minmax(0, 1fr); }
92
+ .ts-owasp-mid { grid-column: 1 / -1; }
79
93
  .ex-band-chips { grid-column: 1 / -1; justify-content: flex-start; }
80
94
  .ex-route-controls { align-items: stretch; }
81
95
  .ex-route-controls .btn { width: 100%; }
@@ -0,0 +1,132 @@
1
+ /* panels/test.css — the Test panel (v1.5.0).
2
+
3
+ /orc-test: five tabs' worth of surface. Loaded by app.html in the order its
4
+ <link> sits in that manifest, and for CSS the load order IS the cascade
5
+ order — which is why 06-responsive and 04-motion come after every panel
6
+ sheet.
7
+
8
+ NO COLOUR TOKEN IS DEFINED HERE. Every one of them is declared once, on bare
9
+ :root, in 00-tokens.css.
10
+
11
+ NO MOTION IS DEFINED HERE either. The run rows reuse `.run-row` / `.run-card`
12
+ from panels/runs.css, so the fold and its reduced-motion behaviour are
13
+ already one implementation rather than two. */
14
+
15
+ /* --- the header strip ---------------------------------------------------- */
16
+ /* Above the tabs, on every one of them: what this run measured, in one line.
17
+ Every value is the CLI's. */
18
+ .ts-strip {
19
+ display: flex;
20
+ flex-wrap: wrap;
21
+ gap: var(--sp-4);
22
+ padding: var(--sp-3);
23
+ border: 1px solid var(--line-soft);
24
+ border-radius: var(--radius-sm);
25
+ margin-bottom: var(--sp-3);
26
+ }
27
+ .ts-strip-item { display: grid; gap: 2px; }
28
+ .ts-strip-value { font-size: 18px; font-weight: 600; font-variant-numeric: tabular-nums; }
29
+ .ts-strip-label { font-size: 11px; color: var(--text-faint); text-transform: uppercase; letter-spacing: .04em; }
30
+ .ts-strip-item.ts-ok .ts-strip-value { color: var(--ok); }
31
+ .ts-strip-item.ts-warn .ts-strip-value { color: var(--warn); }
32
+ .ts-strip-item.ts-bad .ts-strip-value { color: var(--bad); }
33
+
34
+ /* --- the caution --------------------------------------------------------- */
35
+ /* OUTSIDE the tabs, because a caution you must hunt for is a caution nobody
36
+ reads. It fires only on something the run actually MEASURED. */
37
+ .ts-caution {
38
+ padding: var(--sp-3);
39
+ border: 1px solid var(--bad);
40
+ border-radius: var(--radius-sm);
41
+ background: var(--surface-2);
42
+ margin-bottom: var(--sp-3);
43
+ }
44
+ .ts-caution-line { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
45
+
46
+ /* --- the authorization block --------------------------------------------- */
47
+ /* It renders ALWAYS and verbatim. A statement nobody filled in is a STATE, and
48
+ it is drawn as one — never as an empty row. */
49
+ .ts-auth {
50
+ display: grid;
51
+ gap: 2px;
52
+ padding: var(--sp-3);
53
+ border-left: 3px solid var(--ok);
54
+ background: var(--surface-2);
55
+ border-radius: var(--radius-sm);
56
+ margin: var(--sp-3) 0;
57
+ }
58
+ .ts-auth-empty { border-left-color: var(--warn); }
59
+ .ts-auth-label { font-size: 11px; color: var(--text-faint); text-transform: uppercase; letter-spacing: .04em; }
60
+ .ts-auth-value { font-size: 14px; }
61
+ .ts-auth-empty .ts-auth-value { color: var(--warn); }
62
+
63
+ /* The hand-back. ORC does not repair the system it is testing, and the sentence
64
+ that says so is not decoration. */
65
+ .ts-handback {
66
+ padding: var(--sp-2) var(--sp-3);
67
+ border-left: 3px solid var(--warn);
68
+ background: var(--surface-2);
69
+ border-radius: var(--radius-sm);
70
+ font-size: 13px;
71
+ margin: var(--sp-2) 0;
72
+ }
73
+
74
+ .ts-label { font-size: 11px; color: var(--text-faint); text-transform: uppercase; letter-spacing: .04em; }
75
+ .ts-where { color: var(--text-dim); font-family: var(--mono); font-size: 12px; }
76
+
77
+ /* --- the surface --------------------------------------------------------- */
78
+ /* A ROUTE ROW DECLARES ITS COLUMNS. Every route carries the same five children
79
+ in every state — method, path, two chips, and where it came from — so the
80
+ template is fixed and a chip can never land in a slack row. */
81
+ .ts-route {
82
+ display: grid;
83
+ grid-template-columns: 60px minmax(0, 1fr) auto auto minmax(0, 1fr);
84
+ align-items: center;
85
+ gap: var(--sp-2);
86
+ padding: var(--sp-2) 0;
87
+ border-top: 1px solid var(--line-soft);
88
+ }
89
+ .ts-route:first-of-type { border-top: 0; }
90
+ .ts-method { font-family: var(--mono); font-size: 12px; font-weight: 600; color: var(--accent); }
91
+ .ts-path { font-family: var(--mono); font-size: 13px; overflow-wrap: anywhere; }
92
+ .ts-route-min { font-family: var(--mono); font-size: 12px; color: var(--text-dim); padding: 2px 0; overflow-wrap: anywhere; }
93
+ .ts-bad { color: var(--bad); }
94
+
95
+ /* --- the case matrix ----------------------------------------------------- */
96
+ /* Same rule as the route row: FIXED columns, and the tail is a container that
97
+ is always present even when it holds nothing. A card whose child count
98
+ changes with its state must not declare its rows (`.ex-tool`'s 250px
99
+ ellipse), so the count here does not change. */
100
+ .ts-case {
101
+ display: grid;
102
+ grid-template-columns: 64px auto auto minmax(0, 1fr) auto;
103
+ align-items: start;
104
+ gap: var(--sp-2);
105
+ padding: var(--sp-2) 0;
106
+ border-top: 1px solid var(--line-soft);
107
+ }
108
+ .ts-case:first-of-type { border-top: 0; }
109
+ .ts-case-id { font-family: var(--mono); font-size: 12px; font-weight: 600; }
110
+ .ts-case-mid { display: grid; gap: 2px; min-width: 0; }
111
+ .ts-case-target { font-family: var(--mono); font-size: 13px; overflow-wrap: anywhere; }
112
+ .ts-case-why { font-size: 12px; color: var(--text-dim); overflow-wrap: anywhere; }
113
+ .ts-case-gap { font-size: 12px; color: var(--warn); overflow-wrap: anywhere; }
114
+ .ts-case-ev { font-family: var(--mono); font-size: 11px; color: var(--text-faint); overflow-wrap: anywhere; }
115
+ .ts-case-tail { display: flex; flex-wrap: wrap; gap: var(--sp-1); justify-content: flex-end; }
116
+
117
+ /* --- the closed OWASP set ------------------------------------------------ */
118
+ /* TEN ROWS, ALWAYS. An `unchecked` row keeps its slot and carries its reason;
119
+ it is drawn quieter than a FOUND row and it is never drawn as absent. */
120
+ .ts-owasp {
121
+ display: grid;
122
+ grid-template-columns: 64px auto minmax(0, 1fr);
123
+ align-items: start;
124
+ gap: var(--sp-2);
125
+ padding: var(--sp-2) 0;
126
+ border-top: 1px solid var(--line-soft);
127
+ }
128
+ .ts-owasp:first-of-type { border-top: 0; }
129
+ .ts-owasp-id { font-family: var(--mono); font-size: 12px; font-weight: 600; }
130
+ .ts-owasp-mid { display: grid; gap: 3px; min-width: 0; }
131
+ .ts-owasp-name { font-size: 13px; }
132
+ .ts-owasp-unchecked { font-size: 12px; color: var(--text-dim); overflow-wrap: anywhere; }
@@ -36,6 +36,7 @@ const { laneList, lanePhases, laneCalls } = require("./lanes.js");
36
36
  const { diy } = require("./flow.js");
37
37
  const { crosslink } = require("./crosslink.js");
38
38
  const { mockDetail } = require("./mockrun.js");
39
+ const { testList, testShows, testShowToy, testUiTools, testEnvUnhealthy, testEnvAbsent, testEnvRemote } = require("./test.js");
39
40
  const { extraProviders, extraList, extraListNoConnection, extraListNeverTested, extraTools, extraKeyhelp, extraModels, extraDoctor, extraRoute, extraRole, extraLanes, extraStats, extraRates, extraPingOk, extraPingBad, extraPingSaveOffer, extraJournal, extraReconcile, extraJournalPrune, extraPingLive, extraPingDeadModel, extraPingNotInstalled, extraInstall, extraDemotion } = require("./extra.js");
40
41
 
41
42
  module.exports.get = function get(route, q) {
@@ -50,6 +51,15 @@ module.exports.get = function get(route, q) {
50
51
  // a required reason is collected) could never be designed.
51
52
  case "/api/extra/demotion":
52
53
  return extraDemotion;
54
+ // v1.5.0 — /orc-test. An unknown slug falls to the RED run rather than
55
+ // returning nothing: a fixture that 404s teaches the panel a state the CLI
56
+ // does not have (`orc test show` exits 2 and says so in words).
57
+ case "/api/test":
58
+ return testList;
59
+ case "/api/test/one":
60
+ return testShows[(q && q.slug) || ""] || testShowToy;
61
+ case "/api/test/ui":
62
+ return testUiTools;
53
63
  case "/api/lanes":
54
64
  return laneList;
55
65
  case "/api/lane/phases":
@@ -496,11 +506,23 @@ module.exports.get = function get(route, q) {
496
506
  // from the profile so they agree with what the list already claims.
497
507
  //
498
508
  // A route with no entry here still gets the ordinary "nothing ran" reply.
509
+ // v1.5.0 — the /orc-test environment probe. Its OUTCOME is the state worth
510
+ // designing — `unhealthy` is the one where ORC stops and hands the machine back
511
+ // — so it answers with canned data rather than "nothing ran", on the
512
+ // `orc extra ping` precedent. Keyed by slug so `absent` and the REMOTE refusal
513
+ // are reachable too. `surface` and `report` still answer "nothing ran": their
514
+ // outcome is a file on disk, not a state.
515
+ const TEST_ENV = { "api-users": [1, testEnvUnhealthy], "orders-api": [1, testEnvAbsent], "staging-checkout": [2, testEnvRemote] };
516
+
499
517
  module.exports.post = function post(route, body) {
500
518
  // `add` answers OK so the CONNECT FLOW can be walked end to end in fixture
501
519
  // mode — the add is only the step before the test, and the test's outcome is
502
520
  // the state worth designing. Every other write still answers "nothing ran".
503
- if (route === "/api/extra/add") return { exit_code: 0, data: { ok: true, next: "orc extra ping" } };
521
+ if (route === "/api/test/env") {
522
+ const [exit_code, data] = TEST_ENV[String((body && body.slug) || "")] || TEST_ENV["api-users"];
523
+ return { exit_code, data };
524
+ }
525
+ if (route === "/api/extra/add") return { exit_code: 0, data: { ok: true, next: "orc extra ping" } };
504
526
  // v0.51.0 — a launch that could NOT happen and one that did. Both are exit 0,
505
527
  // because "no terminal here" is an ANSWER carrying the command to paste.
506
528
  if (route === "/api/extra/install")