rcf-lite 0.13.0 → 0.15.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 (166) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/bin/rcf.js +3 -1
  3. package/bin/view-supervisor-child.mjs +0 -0
  4. package/blueprints/application-api-rest/docs/topics.md +1 -1
  5. package/blueprints/application-spa/README.md +3 -3
  6. package/blueprints/application-spa/contributions/adrs/adr-202-application-spa-theming.json +1 -1
  7. package/blueprints/application-spa/contributions/adrs/adr-206-application-spa-iconography.json +1 -1
  8. package/blueprints/application-spa/contributions/tacs/tac-207-application-spa-token-adherence-probe.json +7 -7
  9. package/blueprints/application-spa/contributions/tacs/tac-208-application-spa-icon-adherence-probe.json +7 -7
  10. package/blueprints/application-spa/contributions/tacs/tac-209-application-spa-csp-styled-adherence-probe.json +8 -8
  11. package/blueprints/application-spa/contributions/tacs/tac-210-application-spa-external-dependency-provisioning-probe.json +8 -8
  12. package/blueprints/application-spa/contributions/tacs/tac-211-application-spa-core-flow-e2e-probe.json +8 -8
  13. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1129.json +2 -2
  14. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1130.json +2 -2
  15. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1131.json +2 -2
  16. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1132.json +2 -2
  17. package/blueprints/application-spa/contributions/user-stories/application-spa-us-1133.json +3 -3
  18. package/blueprints/application-spa/docs/topics.md +1 -1
  19. package/blueprints/delivery-ci-workflows/CHANGELOG.md +39 -0
  20. package/blueprints/delivery-ci-workflows/README.md +61 -0
  21. package/blueprints/delivery-ci-workflows/assets/bootstrap/README.md +26 -0
  22. package/blueprints/delivery-ci-workflows/assets/bootstrap/adr-bootstrap-coverage-supersession.template.json +28 -0
  23. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/default-branch-checks.yml +61 -0
  24. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +74 -0
  25. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/release.yml +75 -0
  26. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/scheduled-audit.yml +65 -0
  27. package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/notes.md +61 -0
  28. package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/per-gate.json +3 -2
  29. package/blueprints/delivery-ci-workflows/blueprint.json +87 -0
  30. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-701-delivery-ci-workflows-ci-gates.json +30 -0
  31. package/blueprints/{ci-pipeline/contributions/adrs/adr-702-ci-pipeline-strict-coverage-gate.json → delivery-ci-workflows/contributions/adrs/adr-702-delivery-ci-workflows-strict-coverage-gate.json} +4 -4
  32. package/blueprints/{ci-pipeline/contributions/adrs/adr-703-ci-pipeline-node-only-runner.json → delivery-ci-workflows/contributions/adrs/adr-703-delivery-ci-workflows-node-only-runner.json} +1 -1
  33. package/blueprints/{ci-pipeline/contributions/adrs/adr-704-ci-pipeline-report-shape.json → delivery-ci-workflows/contributions/adrs/adr-704-delivery-ci-workflows-report-shape.json} +3 -3
  34. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-705-delivery-ci-workflows-elicitation-surface.json +25 -0
  35. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-706-delivery-ci-workflows-branch-model-defaults.json +25 -0
  36. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-707-delivery-ci-workflows-release-workflow-shape.json +25 -0
  37. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-708-delivery-ci-workflows-provider-hint-shape.json +25 -0
  38. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-709-delivery-ci-workflows-release-artefacts.json +25 -0
  39. package/blueprints/delivery-ci-workflows/contributions/adrs/adr-710-delivery-ci-workflows-scheduled-audit.json +25 -0
  40. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-001.json +18 -0
  41. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-002.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-002.json} +2 -2
  42. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-003.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-003.json} +2 -2
  43. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-004.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-004.json} +2 -2
  44. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-005.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-005.json} +4 -4
  45. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-006.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-006.json} +2 -2
  46. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-007.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-007.json} +2 -2
  47. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-008.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-008.json} +2 -2
  48. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-009.json +18 -0
  49. package/blueprints/{ci-pipeline/contributions/requirements/ci-pipeline-req-010.json → delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-010.json} +2 -2
  50. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-011.json +18 -0
  51. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-012.json +18 -0
  52. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-013.json +18 -0
  53. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-014.json +18 -0
  54. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-015.json +18 -0
  55. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-016.json +18 -0
  56. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-017.json +18 -0
  57. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-018.json +18 -0
  58. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-019.json +18 -0
  59. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-020.json +18 -0
  60. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-021.json +18 -0
  61. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-022.json +18 -0
  62. package/blueprints/delivery-ci-workflows/contributions/requirements/delivery-ci-workflows-req-023.json +18 -0
  63. package/blueprints/{ci-pipeline/contributions/tacs/tac-701-ci-pipeline-gate-runner.json → delivery-ci-workflows/contributions/tacs/tac-701-delivery-ci-workflows-gate-runner.json} +5 -5
  64. package/blueprints/{ci-pipeline/contributions/tacs/tac-702-ci-pipeline-gate-report.json → delivery-ci-workflows/contributions/tacs/tac-702-delivery-ci-workflows-gate-report.json} +1 -1
  65. package/blueprints/{ci-pipeline/contributions/tacs/tac-703-ci-pipeline-aggregate-report.json → delivery-ci-workflows/contributions/tacs/tac-703-delivery-ci-workflows-aggregate-report.json} +2 -2
  66. package/blueprints/delivery-ci-workflows/contributions/tacs/tac-704-delivery-ci-workflows-workflow-materialiser.json +67 -0
  67. package/blueprints/delivery-ci-workflows/contributions/tacs/tac-705-delivery-ci-workflows-release-workflow.json +51 -0
  68. package/blueprints/delivery-ci-workflows/contributions/tacs/tac-706-delivery-ci-workflows-scheduled-audit.json +38 -0
  69. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6101.json +37 -0
  70. package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6102.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6102.json} +3 -3
  71. package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6103.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6103.json} +4 -4
  72. package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6104.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6104.json} +4 -4
  73. package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6105.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6105.json} +6 -6
  74. package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6106.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6106.json} +3 -3
  75. package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6107.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6107.json} +5 -5
  76. package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6108.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6108.json} +4 -4
  77. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6109.json +36 -0
  78. package/blueprints/{ci-pipeline/contributions/user-stories/ci-pipeline-us-6110.json → delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6110.json} +3 -3
  79. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6111.json +36 -0
  80. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6112.json +36 -0
  81. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6113.json +36 -0
  82. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6114.json +46 -0
  83. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6115.json +37 -0
  84. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6116.json +28 -0
  85. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6117.json +28 -0
  86. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6118.json +37 -0
  87. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6119.json +28 -0
  88. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6120.json +28 -0
  89. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6121.json +46 -0
  90. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6122.json +46 -0
  91. package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6123.json +37 -0
  92. package/blueprints/delivery-ci-workflows/docs/topics.md +61 -0
  93. package/blueprints/delivery-ci-workflows/guide/delivery-ci-workflows.md +174 -0
  94. package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -3
  95. package/blueprints/email-smtp-resend/docs/topics.md +1 -1
  96. package/blueprints/observability-essentials/README.md +2 -2
  97. package/blueprints/observability-essentials/docs/topics.md +5 -5
  98. package/blueprints/observability-probe-endpoints/docs/topics.md +2 -2
  99. package/blueprints/persistence-data-d1/README.md +2 -2
  100. package/blueprints/persistence-data-d1/assets/facade-shape/facade-module-shape.md +1 -1
  101. package/blueprints/persistence-data-d1/contributions/tacs/tac-1403-persistence-data-d1-deploy-gate.json +1 -1
  102. package/blueprints/persistence-data-d1/docs/topics.md +2 -2
  103. package/blueprints/persistence-data-d1/guide/persistence-data-d1.md +1 -1
  104. package/blueprints/persistence-data-sqlite/README.md +1 -1
  105. package/blueprints/persistence-data-sqlite/docs/topics.md +1 -1
  106. package/blueprints/security-auth-clerk/README.md +5 -3
  107. package/blueprints/security-auth-clerk/assets/middleware/workers-fetch-shape.md +123 -0
  108. package/blueprints/security-auth-clerk/assets/wiring/workers-wrangler-toml-shape.md +51 -0
  109. package/blueprints/security-auth-clerk/blueprint.json +1 -1
  110. package/blueprints/security-auth-clerk/docs/topics.md +2 -2
  111. package/blueprints/security-auth-clerk/guide/security-auth-clerk.md +9 -0
  112. package/blueprints/security-auth-keycloak/docs/topics.md +2 -2
  113. package/blueprints/security-auth-magic-link/README.md +1 -1
  114. package/blueprints/security-auth-magic-link/docs/topics.md +1 -1
  115. package/blueprints/security-auth-oauth2/README.md +1 -1
  116. package/blueprints/security-auth-oauth2/docs/topics.md +2 -2
  117. package/blueprints/security-secrets-management/README.md +1 -1
  118. package/blueprints/security-secrets-management/docs/topics.md +3 -3
  119. package/guidance/build-cycle-playbook.md +2 -2
  120. package/guidance/document-model.md +1 -1
  121. package/guidance/harness-template.md +13 -0
  122. package/guidance/managed/agent-instructions-block.hash +1 -1
  123. package/guidance/managed/agent-instructions-block.md +13 -0
  124. package/package.json +15 -14
  125. package/rcf/adrs/adr-001.json +1 -1
  126. package/rcf/adrs/adr-009.json +1 -1
  127. package/rcf/build-sequence.json +1 -1
  128. package/rcf/manifest.json +2 -2
  129. package/rcf/prd.json +2 -2
  130. package/releases/releases.yaml +126 -0
  131. package/src/blueprint/apply.js +60 -13
  132. package/src/blueprint/index.js +12 -0
  133. package/src/blueprint/library-loader.js +292 -0
  134. package/src/blueprint/library-registry.js +341 -0
  135. package/src/blueprint/list.js +38 -4
  136. package/src/blueprint/shelf-resolver.js +144 -31
  137. package/src/blueprint/supersede.js +56 -13
  138. package/src/cli/blueprint-library.js +447 -0
  139. package/src/cli/blueprint.js +50 -9
  140. package/src/cli/guidance.js +1 -1
  141. package/src/cli/help.js +27 -1
  142. package/src/cli/version.js +673 -0
  143. package/src/cli/view.js +282 -1
  144. package/src/server/index.js +3 -0
  145. package/src/server/routes.js +15 -1
  146. package/src/server/scope-endpoint.js +105 -0
  147. package/src/view/live-client.js +253 -6
  148. package/src/view/scope.js +231 -0
  149. package/src/view/style.css +42 -0
  150. package/blueprints/ci-pipeline/README.md +0 -49
  151. package/blueprints/ci-pipeline/assets/ci-provider-examples/github-actions.yml +0 -61
  152. package/blueprints/ci-pipeline/assets/ci-provider-examples/notes.md +0 -50
  153. package/blueprints/ci-pipeline/blueprint.json +0 -46
  154. package/blueprints/ci-pipeline/contributions/adrs/adr-701-ci-pipeline-ci-gates.json +0 -25
  155. package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-001.json +0 -18
  156. package/blueprints/ci-pipeline/contributions/requirements/ci-pipeline-req-009.json +0 -18
  157. package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6101.json +0 -37
  158. package/blueprints/ci-pipeline/contributions/user-stories/ci-pipeline-us-6109.json +0 -36
  159. package/blueprints/ci-pipeline/docs/topics.md +0 -49
  160. package/blueprints/ci-pipeline/guide/ci-pipeline.md +0 -79
  161. package/rcf/.identity/profile.md +0 -37
  162. package/rcf/knowledge/INDEX.md +0 -12
  163. package/rcf/knowledge/README.md +0 -41
  164. package/rcf/knowledge/docs/.gitkeep +0 -0
  165. package/rcf/knowledge/notes/.gitkeep +0 -0
  166. /package/blueprints/{ci-pipeline → delivery-ci-workflows}/assets/report-samples/pipeline.json +0 -0
package/src/cli/view.js CHANGED
@@ -5,7 +5,9 @@
5
5
  // exported from that bin now live here so the CLI tests can still
6
6
  // exercise them without spawning a subprocess.
7
7
 
8
+ import { readFile } from 'node:fs/promises';
8
9
  import { spawn } from 'node:child_process';
10
+ import { join } from 'node:path';
9
11
  import { platform } from 'node:process';
10
12
 
11
13
  import { formatErrors } from '#core/errors';
@@ -20,6 +22,8 @@ import {
20
22
  readLogTail,
21
23
  } from '../view-supervisor/index.js';
22
24
  import { parsePersistUntil } from '../view-supervisor/persist-until.js';
25
+ // Deep-link scope resolution (w-2026-08-30-dave-020).
26
+ import { contributionsForBlueprint, listAppliedSlugs, treeHasId } from '../view/scope.js';
23
27
 
24
28
  export const DEFAULT_PORT = 4373;
25
29
  export const SHUTDOWN_BUDGET_MS = 2000;
@@ -58,6 +62,19 @@ Subverbs (spec §9.2):
58
62
  record.
59
63
  rcf audit view logs [--tail <n>]
60
64
  Print the supervisor log tail (default 200).
65
+ rcf audit view open [--blueprint <slug>] [--node <id>] [--no-open] [--json]
66
+ Resolve a scoped deep-link into the running
67
+ (or default) view server and print the URL.
68
+ --blueprint scopes the tree to the applied
69
+ blueprint's namespaced contribution list;
70
+ --node focuses a single doc id (any kind in
71
+ the tree). Both compose. Prints the URL to
72
+ stdout every time; opens it in the default
73
+ browser too when the terminal is a TTY and
74
+ --no-open was not passed. --json emits the
75
+ resolution shape for scripting. Refuses (exit
76
+ 2) if the slug is not applied or the node id
77
+ is not on the tree.
61
78
 
62
79
  Options:
63
80
  --port <n> Bind the HTTP server on the given port.
@@ -96,7 +113,7 @@ Exit codes:
96
113
  130 SIGINT
97
114
  `;
98
115
 
99
- const SUBVERBS = new Set(['start', 'status', 'stop', 'logs']);
116
+ const SUBVERBS = new Set(['start', 'status', 'stop', 'logs', 'open']);
100
117
 
101
118
  /**
102
119
  * Parse the view subcommand argv. Hand-rolled so the CLI can pass
@@ -375,6 +392,10 @@ async function runSubverb({ subverb, argv, deps }) {
375
392
  return 0;
376
393
  }
377
394
 
395
+ if (subverb === 'open') {
396
+ return await runOpenSubverb({ argv, projectRoot, deps: { env, stdout, stderr } });
397
+ }
398
+
378
399
  if (subverb === 'logs') {
379
400
  const tailIdx = argv.indexOf('--tail');
380
401
  const tail = tailIdx >= 0 ? Number.parseInt(argv[tailIdx + 1] ?? '', 10) : 200;
@@ -452,3 +473,263 @@ async function runSubverb({ subverb, argv, deps }) {
452
473
  return 1;
453
474
  }
454
475
  }
476
+
477
+ // -------------------------------------------------------------------
478
+ // `rcf audit view open` (w-2026-08-30-dave-020).
479
+ //
480
+ // Resolves a scoped deep-link into the running (or default) view
481
+ // server and prints the URL. The agent-facing surface: during a
482
+ // blueprint integration validation gate, the agent runs this to hand
483
+ // the operator a URL that opens the viewer with the relevant
484
+ // contribution set already visible (rest dimmed) and the target node
485
+ // scrolled into view. Read-only: v1 does not accept approve/reject
486
+ // state; that is a v2 concern (state-bearing, needs its own design).
487
+ // -------------------------------------------------------------------
488
+
489
+ /**
490
+ * Parse the argv slice for `open`. Kept out of `parseArgs` because
491
+ * the flag set overlaps enough to conflate error messages.
492
+ *
493
+ * @param {string[]} argv
494
+ * @returns {{ opts: object, errors: string[] }}
495
+ */
496
+ export function parseOpenArgs(argv) {
497
+ const opts = {
498
+ blueprint: null,
499
+ node: null,
500
+ noOpen: false,
501
+ json: false,
502
+ port: null,
503
+ help: false,
504
+ };
505
+ const errors = [];
506
+ for (let i = 0; i < argv.length; i += 1) {
507
+ const arg = argv[i];
508
+ switch (arg) {
509
+ case '--no-open':
510
+ opts.noOpen = true;
511
+ break;
512
+ case '--json':
513
+ opts.json = true;
514
+ break;
515
+ case '--help':
516
+ case '-h':
517
+ opts.help = true;
518
+ break;
519
+ case '--blueprint': {
520
+ const next = argv[i + 1];
521
+ if (next === undefined || next.startsWith('--')) {
522
+ errors.push('--blueprint requires a slug argument');
523
+ break;
524
+ }
525
+ opts.blueprint = next;
526
+ i += 1;
527
+ break;
528
+ }
529
+ case '--node': {
530
+ const next = argv[i + 1];
531
+ if (next === undefined || next.startsWith('--')) {
532
+ errors.push('--node requires a doc id argument');
533
+ break;
534
+ }
535
+ opts.node = next;
536
+ i += 1;
537
+ break;
538
+ }
539
+ case '--port': {
540
+ const next = argv[i + 1];
541
+ if (next === undefined || next.startsWith('--')) {
542
+ errors.push('--port requires a numeric argument');
543
+ break;
544
+ }
545
+ const n = Number(next);
546
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
547
+ errors.push(`--port expects an integer in [0, 65535], got ${next}`);
548
+ } else {
549
+ opts.port = n;
550
+ }
551
+ i += 1;
552
+ break;
553
+ }
554
+ default:
555
+ errors.push(`unknown option: ${arg}`);
556
+ }
557
+ }
558
+ return { opts, errors };
559
+ }
560
+
561
+ /**
562
+ * Read `rcf/manifest.json` off disk (bypasses the walker so the CLI
563
+ * can validate scope fast without a full tree walk when only a slug
564
+ * check is needed).
565
+ *
566
+ * @param {string} projectRoot
567
+ * @returns {Promise<object|null>}
568
+ */
569
+ async function readManifestFile(projectRoot) {
570
+ try {
571
+ const raw = await readFile(join(projectRoot, 'rcf', 'manifest.json'), 'utf8');
572
+ return JSON.parse(raw);
573
+ } catch (err) {
574
+ if (err && err.code === 'ENOENT') return null;
575
+ throw err;
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Compose a scoped URL from the base URL plus scope params.
581
+ *
582
+ * @param {object} args
583
+ * @param {string} args.baseUrl - e.g. `http://127.0.0.1:4373/`
584
+ * @param {string|null} args.blueprint
585
+ * @param {string|null} args.node
586
+ * @returns {string}
587
+ */
588
+ export function composeScopeUrl({ baseUrl, blueprint, node }) {
589
+ // The base URL from the supervisor / --port / env / default resolver
590
+ // always ends with `/`. Normalise defensively so a caller-supplied
591
+ // base without one still composes to a valid URL.
592
+ const trimmed = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
593
+ const params = [];
594
+ if (blueprint) params.push(`blueprint=${encodeURIComponent(blueprint)}`);
595
+ if (node) params.push(`node=${encodeURIComponent(node)}`);
596
+ const query = params.length > 0 ? `?${params.join('&')}` : '';
597
+ // Hash so the existing hash-router activates the tab + drills the
598
+ // ancestor <details> chain without waiting for the client to fetch
599
+ // /scope.json.
600
+ const hash = node ? `#${encodeURIComponent(node)}` : '';
601
+ return `${trimmed}${query}${hash}`;
602
+ }
603
+
604
+ /**
605
+ * Resolve the base URL to open at. Prefers a running detached
606
+ * supervisor's recorded URL (respects any non-default --port a prior
607
+ * start invocation bound to); otherwise the RCF_VIEW_PORT / default
608
+ * pair. The command still prints a URL even when no server is
609
+ * running, so an agent can print one to the operator alongside a
610
+ * hint to start the server.
611
+ *
612
+ * @param {object} args
613
+ * @param {string} args.projectRoot
614
+ * @param {number|null} args.port
615
+ * @param {NodeJS.ProcessEnv} args.env
616
+ * @returns {Promise<{ baseUrl: string, source: 'supervisor'|'flag'|'env'|'default' }>}
617
+ */
618
+ export async function resolveBaseUrl({ projectRoot, port, env }) {
619
+ if (typeof port === 'number') {
620
+ return { baseUrl: `http://127.0.0.1:${port}/`, source: 'flag' };
621
+ }
622
+ try {
623
+ const status = await statusOfDetached(projectRoot);
624
+ if (status && status.state === 'running' && typeof status.url === 'string' && status.url.length > 0) {
625
+ const url = status.url.endsWith('/') ? status.url : `${status.url}/`;
626
+ return { baseUrl: url, source: 'supervisor' };
627
+ }
628
+ } catch { /* fall through */ }
629
+ const raw = env.RCF_VIEW_PORT;
630
+ if (raw !== undefined && raw !== '') {
631
+ const n = Number(raw);
632
+ if (Number.isInteger(n) && n >= 0 && n <= 65535) {
633
+ return { baseUrl: `http://127.0.0.1:${n}/`, source: 'env' };
634
+ }
635
+ }
636
+ return { baseUrl: `http://127.0.0.1:${DEFAULT_PORT}/`, source: 'default' };
637
+ }
638
+
639
+ /**
640
+ * The full open flow. Kept as a named export so the test suite can
641
+ * exercise it directly.
642
+ *
643
+ * @param {object} args
644
+ * @param {string[]} args.argv
645
+ * @param {string} args.projectRoot
646
+ * @param {{ env: NodeJS.ProcessEnv, stdout: NodeJS.WritableStream, stderr: NodeJS.WritableStream }} args.deps
647
+ * @returns {Promise<number>}
648
+ */
649
+ export async function runOpenSubverb({ argv, projectRoot, deps }) {
650
+ const { env, stdout, stderr } = deps;
651
+ const { opts, errors: argErrors } = parseOpenArgs(argv);
652
+ if (opts.help) {
653
+ stdout.write(HELP);
654
+ return 0;
655
+ }
656
+ if (argErrors.length > 0) {
657
+ for (const msg of argErrors) stderr.write(`[error] usage view open: ${msg}\n`);
658
+ return 2;
659
+ }
660
+ if (!opts.blueprint && !opts.node) {
661
+ stderr.write('[error] usage view open: pass --blueprint <slug> and/or --node <id>\n');
662
+ return 2;
663
+ }
664
+
665
+ // Validate scope.
666
+ let manifest = null;
667
+ if (opts.blueprint) {
668
+ manifest = await readManifestFile(projectRoot);
669
+ const hit = contributionsForBlueprint(manifest, opts.blueprint);
670
+ if (!hit.found) {
671
+ const applied = listAppliedSlugs(manifest);
672
+ const hint = applied.length > 0
673
+ ? ` Applied: ${applied.join(', ')}.`
674
+ : ' No blueprints are applied to this project.';
675
+ stderr.write(`[error] usage view open: blueprint '${opts.blueprint}' is not applied to this project.${hint}\n`);
676
+ return 2;
677
+ }
678
+ }
679
+ let contributionIds = [];
680
+ if (manifest && opts.blueprint) {
681
+ const hit = contributionsForBlueprint(manifest, opts.blueprint);
682
+ contributionIds = hit.found ? hit.contributionIds : [];
683
+ }
684
+
685
+ if (opts.node) {
686
+ // Walk the tree once to prove the node exists. Cheaper than
687
+ // guessing at grammar; also gives the user the same "unknown id"
688
+ // wording as define/link.
689
+ const { tree } = await walkTree({ projectRoot });
690
+ if (!treeHasId(tree, opts.node)) {
691
+ stderr.write(`[error] usage view open: node id '${opts.node}' is not present on the tree.\n`);
692
+ return 2;
693
+ }
694
+ }
695
+
696
+ const { baseUrl, source } = await resolveBaseUrl({ projectRoot, port: opts.port, env });
697
+ const url = composeScopeUrl({ baseUrl, blueprint: opts.blueprint, node: opts.node });
698
+ const supervisor = source === 'supervisor';
699
+ const serverRunningNote = supervisor
700
+ ? null
701
+ : 'view server is not running; start it with `rcf audit view start --detach` for the URL to load';
702
+
703
+ if (opts.json) {
704
+ stdout.write(`${JSON.stringify({
705
+ url,
706
+ baseUrl,
707
+ urlSource: source,
708
+ blueprint: opts.blueprint,
709
+ node: opts.node,
710
+ contributionCount: contributionIds.length,
711
+ opened: false,
712
+ serverRunning: supervisor,
713
+ }, null, 2)}\n`);
714
+ } else {
715
+ stdout.write(`${url}\n`);
716
+ if (opts.blueprint) {
717
+ stdout.write(` blueprint: ${opts.blueprint} (${contributionIds.length} contribution${contributionIds.length === 1 ? '' : 's'})\n`);
718
+ }
719
+ if (opts.node) {
720
+ stdout.write(` node: ${opts.node}\n`);
721
+ }
722
+ if (serverRunningNote) stdout.write(` note: ${serverRunningNote}\n`);
723
+ }
724
+
725
+ if (!opts.noOpen) {
726
+ maybeAutoOpen({
727
+ target: url,
728
+ noOpen: opts.noOpen,
729
+ stream: stdout,
730
+ env,
731
+ stderr,
732
+ });
733
+ }
734
+ return 0;
735
+ }
@@ -16,6 +16,7 @@ import { join } from 'node:path';
16
16
  import { LIVE_CLIENT_PATH, STYLE_CSS_PATH, VENDORED_MERMAID_PATH, renderModelToPage } from '../view/index.js';
17
17
  import { watch as defaultWatch } from '../watch/index.js';
18
18
  import { createRouter } from './routes.js';
19
+ import { createScopeHandler } from './scope-endpoint.js';
19
20
  import { createSseHub } from './sse.js';
20
21
 
21
22
  /**
@@ -96,12 +97,14 @@ export async function startServer(args) {
96
97
  // and the first SSE connect gets a real payload.
97
98
  await rewalk();
98
99
 
100
+ const scopeHandler = createScopeHandler({ projectRoot });
99
101
  const router = createRouter({
100
102
  currentState: () => state,
101
103
  sse,
102
104
  stylePath: STYLE_CSS_PATH,
103
105
  mermaidPath: VENDORED_MERMAID_PATH,
104
106
  liveClientPath: LIVE_CLIENT_PATH,
107
+ scope: scopeHandler,
105
108
  });
106
109
 
107
110
  const server = createServer(router);
@@ -1,9 +1,10 @@
1
- // HTTP route table for the live-view server. Only five routes are served:
1
+ // HTTP route table for the live-view server. Routes served:
2
2
  // GET / -> rendered page (text/html)
3
3
  // GET /events -> SSE stream (text/event-stream)
4
4
  // GET /style.css -> shipped stylesheet
5
5
  // GET /mermaid.min.js-> vendored mermaid runtime
6
6
  // GET /live-client.js-> phase 3.8 live client script
7
+ // GET /scope.json -> deep-link scope resolver (w-2026-08-30-dave-020)
7
8
  // Everything else -> 404 text/plain.
8
9
  //
9
10
  // No CORS headers, no cache headers on static assets beyond what the
@@ -25,6 +26,10 @@ const MIME = {
25
26
  * @property {string} stylePath
26
27
  * @property {string} mermaidPath
27
28
  * @property {string} liveClientPath
29
+ * @property {((req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void>|void)} [scope]
30
+ * Deep-link scope handler (w-2026-08-30-dave-020). Optional so
31
+ * the router stays usable in tests that mount only the static
32
+ * routes without a project root.
28
33
  */
29
34
 
30
35
  /**
@@ -74,6 +79,15 @@ export function createRouter(deps) {
74
79
  serveFile(res, deps.liveClientPath, MIME.js).catch((err) => fail(res, err));
75
80
  return;
76
81
  }
82
+ if (path === '/scope.json') {
83
+ if (typeof deps.scope !== 'function') {
84
+ res.writeHead(404, { 'content-type': MIME.txt });
85
+ res.end('not found\n');
86
+ return;
87
+ }
88
+ Promise.resolve(deps.scope(req, res)).catch((err) => fail(res, err));
89
+ return;
90
+ }
77
91
 
78
92
  res.writeHead(404, { 'content-type': MIME.txt });
79
93
  res.end('not found\n');
@@ -0,0 +1,105 @@
1
+ // HTTP handler for `/scope.json`. The live-client fetches this to
2
+ // resolve a `?blueprint=<slug>` URL query into the applied
3
+ // contribution list and the customised-since-apply subset.
4
+ //
5
+ // Three shapes:
6
+ // GET /scope.json -> { blueprints: [{ slug, contributionCount, appliedAt, version }] }
7
+ // (the applied blueprint index; used by the client to know
8
+ // which slugs are valid before firing a scoped request)
9
+ // GET /scope.json?blueprint=<slug> -> { slug, appliedAt, version, contributionIds, customisedIds, missingSourceIds }
10
+ // GET /scope.json?blueprint=<slug> with slug not applied -> 404 { error: 'unknownBlueprint', slug }
11
+ //
12
+ // The endpoint is read-only. It reads manifest.json every call rather
13
+ // than piggy-backing on the cached tree state; the caller is a browser
14
+ // (small payload, low frequency), and a fresh read means scope answers
15
+ // track a re-apply immediately even if a tree-update SSE broadcast has
16
+ // not yet propagated.
17
+
18
+ import { readFile } from 'node:fs/promises';
19
+ import { join } from 'node:path';
20
+
21
+ import { contributionsForBlueprint, detectCustomisations, listAppliedSlugs } from '../view/scope.js';
22
+
23
+ const MIME_JSON = 'application/json; charset=utf-8';
24
+
25
+ /**
26
+ * Build a handler bound to a project root. Returns a function suitable
27
+ * for wiring into the router as the `/scope.json` route.
28
+ *
29
+ * @param {object} args
30
+ * @param {string} args.projectRoot
31
+ * @param {(root: string) => Promise<object|null>} [args._readManifest] - test seam
32
+ * @returns {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void>}
33
+ */
34
+ export function createScopeHandler({ projectRoot, _readManifest, _detectCustomisations } = {}) {
35
+ const readManifest = _readManifest ?? defaultReadManifest;
36
+ const detectImpl = _detectCustomisations ?? detectCustomisations;
37
+ return async function handle(req, res) {
38
+ try {
39
+ const url = new URL(req.url ?? '/', 'http://localhost');
40
+ const slug = normaliseString(url.searchParams.get('blueprint'));
41
+ const manifest = await readManifest(projectRoot);
42
+ if (!slug) {
43
+ respond(res, 200, {
44
+ blueprints: appliedIndex(manifest),
45
+ });
46
+ return;
47
+ }
48
+ const found = contributionsForBlueprint(manifest, slug);
49
+ if (!found.found) {
50
+ respond(res, 404, { error: 'unknownBlueprint', slug });
51
+ return;
52
+ }
53
+ const { customisedIds, missingSourceIds } = await detectImpl({
54
+ projectRoot,
55
+ record: found.record,
56
+ });
57
+ respond(res, 200, {
58
+ slug,
59
+ appliedAt: found.record.appliedAt ?? null,
60
+ version: found.record.version ?? null,
61
+ contributionIds: found.contributionIds,
62
+ customisedIds,
63
+ missingSourceIds,
64
+ });
65
+ } catch (err) {
66
+ respond(res, 500, { error: 'ioFailure', message: err && err.message ? err.message : 'unknown' });
67
+ }
68
+ };
69
+ }
70
+
71
+ function appliedIndex(manifest) {
72
+ const slugs = listAppliedSlugs(manifest);
73
+ const list = manifest?.blueprints ?? [];
74
+ return slugs.map((slug) => {
75
+ const rec = list.find((b) => b?.slug === slug) ?? {};
76
+ return {
77
+ slug,
78
+ version: rec.version ?? null,
79
+ appliedAt: rec.appliedAt ?? null,
80
+ contributionCount: Array.isArray(rec.contributions) ? rec.contributions.length : 0,
81
+ };
82
+ });
83
+ }
84
+
85
+ function normaliseString(v) {
86
+ if (typeof v !== 'string') return null;
87
+ const trimmed = v.trim();
88
+ return trimmed.length === 0 ? null : trimmed;
89
+ }
90
+
91
+ async function defaultReadManifest(projectRoot) {
92
+ try {
93
+ const raw = await readFile(join(projectRoot, 'rcf', 'manifest.json'), 'utf8');
94
+ return JSON.parse(raw);
95
+ } catch (err) {
96
+ if (err && err.code === 'ENOENT') return null;
97
+ throw err;
98
+ }
99
+ }
100
+
101
+ function respond(res, status, body) {
102
+ const buf = Buffer.from(`${JSON.stringify(body)}\n`, 'utf8');
103
+ res.writeHead(status, { 'content-type': MIME_JSON, 'content-length': String(buf.length) });
104
+ res.end(buf);
105
+ }