rcf-lite 0.0.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +288 -0
- package/LICENSE +202 -0
- package/README.md +92 -4
- package/bin/rcf-verify.js +122 -0
- package/bin/rcf.js +174 -0
- package/bin/view-supervisor-child.mjs +14 -0
- package/fixtures/canary-manifest.json +103 -0
- package/guidance/README.md +25 -0
- package/guidance/build-cycle-playbook.md +492 -0
- package/guidance/build-cycle.md +40 -0
- package/guidance/document-model.md +45 -0
- package/guidance/elicitation-playbook.md +442 -0
- package/guidance/harness-template.md +246 -0
- package/guidance/managed/README.md +63 -0
- package/guidance/managed/agent-instructions-block.hash +1 -0
- package/guidance/managed/agent-instructions-block.md +220 -0
- package/guidance/managed/legacy-fragment-hashes.json +22 -0
- package/guidance/manifest.json +21 -0
- package/guidance/overview.md +48 -0
- package/guidance/persona-programme.md +44 -0
- package/package.json +69 -8
- package/rcf/adrs/adr-001.json +25 -0
- package/rcf/adrs/adr-002.json +25 -0
- package/rcf/adrs/adr-003.json +25 -0
- package/rcf/adrs/adr-004.json +20 -0
- package/rcf/adrs/adr-005.json +20 -0
- package/rcf/adrs/adr-006.json +25 -0
- package/rcf/adrs/adr-007.json +25 -0
- package/rcf/adrs/adr-008.json +25 -0
- package/rcf/adrs/adr-009.json +25 -0
- package/rcf/build-sequence.json +11 -0
- package/rcf/code-nodes/cn-001.json +14 -0
- package/rcf/code-nodes/cn-002.json +14 -0
- package/rcf/code-nodes/cn-003.json +15 -0
- package/rcf/code-nodes/cn-004.json +17 -0
- package/rcf/code-nodes/cn-005.json +19 -0
- package/rcf/code-nodes/cn-006.json +17 -0
- package/rcf/code-nodes/cn-007.json +16 -0
- package/rcf/code-nodes/cn-008.json +19 -0
- package/rcf/code-nodes/cn-009.json +16 -0
- package/rcf/code-nodes/cn-010.json +18 -0
- package/rcf/code-nodes/cn-011.json +18 -0
- package/rcf/code-nodes/cn-012.json +18 -0
- package/rcf/code-nodes/cn-013.json +19 -0
- package/rcf/code-nodes/cn-014.json +16 -0
- package/rcf/code-nodes/cn-015.json +16 -0
- package/rcf/code-nodes/cn-016.json +16 -0
- package/rcf/code-nodes/cn-017.json +16 -0
- package/rcf/code-nodes/cn-018.json +15 -0
- package/rcf/code-nodes/cn-019.json +16 -0
- package/rcf/code-nodes/cn-020.json +16 -0
- package/rcf/code-nodes/cn-021.json +16 -0
- package/rcf/code-nodes/cn-022.json +16 -0
- package/rcf/code-nodes/cn-023.json +16 -0
- package/rcf/code-nodes/cn-024.json +16 -0
- package/rcf/code-nodes/cn-025.json +16 -0
- package/rcf/code-nodes/cn-026.json +16 -0
- package/rcf/code-nodes/cn-027.json +16 -0
- package/rcf/code-nodes/cn-028.json +16 -0
- package/rcf/code-nodes/cn-029.json +19 -0
- package/rcf/code-nodes/cn-030.json +14 -0
- package/rcf/code-nodes/cn-031.json +14 -0
- package/rcf/code-nodes/cn-032.json +14 -0
- package/rcf/code-nodes/cn-033.json +14 -0
- package/rcf/code-nodes/cn-034.json +14 -0
- package/rcf/code-nodes/cn-035.json +14 -0
- package/rcf/code-nodes/cn-036.json +14 -0
- package/rcf/code-nodes/cn-037.json +14 -0
- package/rcf/code-nodes/cn-038.json +14 -0
- package/rcf/code-nodes/cn-039.json +14 -0
- package/rcf/code-nodes/cn-040.json +14 -0
- package/rcf/code-nodes/cn-041.json +14 -0
- package/rcf/code-nodes/cn-042.json +14 -0
- package/rcf/code-nodes/cn-043.json +14 -0
- package/rcf/code-nodes/cn-044.json +14 -0
- package/rcf/code-nodes/cn-045.json +14 -0
- package/rcf/code-nodes/cn-046.json +14 -0
- package/rcf/code-nodes/cn-047.json +14 -0
- package/rcf/code-nodes/cn-048.json +14 -0
- package/rcf/code-nodes/cn-049.json +14 -0
- package/rcf/code-nodes/cn-050.json +14 -0
- package/rcf/code-nodes/cn-051.json +14 -0
- package/rcf/code-nodes/cn-052.json +14 -0
- package/rcf/code-nodes/cn-053.json +14 -0
- package/rcf/code-nodes/cn-054.json +14 -0
- package/rcf/code-nodes/cn-055.json +14 -0
- package/rcf/code-nodes/cn-056.json +14 -0
- package/rcf/code-nodes/cn-057.json +14 -0
- package/rcf/fbs/fbs-001.json +49 -0
- package/rcf/fbs/fbs-002.json +42 -0
- package/rcf/fbs/fbs-003.json +37 -0
- package/rcf/fbs/fbs-004.json +39 -0
- package/rcf/fbs/fbs-005.json +38 -0
- package/rcf/fbs/fbs-006.json +48 -0
- package/rcf/fbs/fbs-007.json +39 -0
- package/rcf/fbs/fbs-008.json +40 -0
- package/rcf/fbs/fbs-009.json +36 -0
- package/rcf/fbs/fbs-010.json +41 -0
- package/rcf/fbs/fbs-011.json +36 -0
- package/rcf/fbs/fbs-012.json +46 -0
- package/rcf/fbs/fbs-013.json +42 -0
- package/rcf/fbs/fbs-014.json +49 -0
- package/rcf/fbs/fbs-015.json +32 -0
- package/rcf/manifest.json +17 -0
- package/rcf/prd.json +47 -0
- package/rcf/requirements/req-001.json +19 -0
- package/rcf/requirements/req-002.json +19 -0
- package/rcf/requirements/req-003.json +19 -0
- package/rcf/requirements/req-004.json +19 -0
- package/rcf/requirements/req-005.json +19 -0
- package/rcf/requirements/req-006.json +19 -0
- package/rcf/requirements/req-007.json +19 -0
- package/rcf/requirements/req-008.json +19 -0
- package/rcf/requirements/req-009.json +19 -0
- package/rcf/tacs/tac-001.json +45 -0
- package/rcf/tacs/tac-002.json +109 -0
- package/rcf/tacs/tac-003.json +40 -0
- package/rcf/tacs/tac-004.json +51 -0
- package/rcf/tacs/tac-005.json +52 -0
- package/rcf/tacs/tac-006.json +104 -0
- package/rcf/tacs/tac-007.json +38 -0
- package/rcf/tacs/tac-008.json +51 -0
- package/rcf/tad.json +51 -0
- package/rcf/test-suites/PENDING.md +23 -0
- package/rcf/test-suites/ts-001.json +38 -0
- package/rcf/test-suites/ts-002.json +38 -0
- package/rcf/test-suites/ts-003.json +43 -0
- package/rcf/test-suites/ts-004.json +44 -0
- package/rcf/test-suites/ts-005.json +30 -0
- package/rcf/test-suites/ts-006.json +36 -0
- package/rcf/test-suites/ts-007.json +43 -0
- package/rcf/test-suites/ts-008.json +37 -0
- package/rcf/test-suites/ts-009.json +38 -0
- package/rcf/test-suites/ts-010.json +38 -0
- package/rcf/test-suites/ts-011.json +44 -0
- package/rcf/test-suites/ts-012.json +36 -0
- package/rcf/test-suites/ts-013.json +38 -0
- package/rcf/test-suites/ts-014.json +38 -0
- package/rcf/test-suites/ts-015.json +38 -0
- package/rcf/test-suites/ts-016.json +37 -0
- package/rcf/test-suites/ts-017.json +38 -0
- package/rcf/test-suites/ts-018.json +38 -0
- package/rcf/test-suites/ts-019.json +37 -0
- package/rcf/test-suites/ts-020.json +46 -0
- package/rcf/test-suites/ts-021.json +46 -0
- package/rcf/test-suites/ts-022.json +46 -0
- package/rcf/test-suites/ts-023.json +46 -0
- package/rcf/test-suites/ts-024.json +46 -0
- package/rcf/test-suites/ts-025.json +52 -0
- package/rcf/user-stories/us-101.json +40 -0
- package/rcf/user-stories/us-102.json +40 -0
- package/rcf/user-stories/us-201.json +40 -0
- package/rcf/user-stories/us-202.json +40 -0
- package/rcf/user-stories/us-203.json +32 -0
- package/rcf/user-stories/us-301.json +40 -0
- package/rcf/user-stories/us-302.json +40 -0
- package/rcf/user-stories/us-303.json +40 -0
- package/rcf/user-stories/us-304.json +40 -0
- package/rcf/user-stories/us-401.json +40 -0
- package/rcf/user-stories/us-402.json +40 -0
- package/rcf/user-stories/us-403.json +40 -0
- package/rcf/user-stories/us-501.json +40 -0
- package/rcf/user-stories/us-502.json +40 -0
- package/rcf/user-stories/us-503.json +40 -0
- package/rcf/user-stories/us-601.json +40 -0
- package/rcf/user-stories/us-602.json +40 -0
- package/rcf/user-stories/us-701.json +40 -0
- package/rcf/user-stories/us-702.json +40 -0
- package/rcf/user-stories/us-801.json +49 -0
- package/rcf/user-stories/us-802.json +49 -0
- package/rcf/user-stories/us-803.json +49 -0
- package/rcf/user-stories/us-804.json +49 -0
- package/rcf/user-stories/us-805.json +49 -0
- package/rcf/user-stories/us-901.json +40 -0
- package/src/.gitkeep +0 -0
- package/src/browser-verify/auth-smoke.js +109 -0
- package/src/browser-verify/index.js +29 -0
- package/src/browser-verify/invariants.js +336 -0
- package/src/browser-verify/manifest-writer.js +189 -0
- package/src/browser-verify/runner.js +171 -0
- package/src/build/bundle.js +198 -0
- package/src/build/formatters/json.js +18 -0
- package/src/build/formatters/markdown.js +406 -0
- package/src/build/index.js +14 -0
- package/src/build/mark.js +177 -0
- package/src/build/queue.js +285 -0
- package/src/cli/browser-verify.js +231 -0
- package/src/cli/build.js +584 -0
- package/src/cli/coverage.js +219 -0
- package/src/cli/create.js +355 -0
- package/src/cli/delete.js +127 -0
- package/src/cli/design.js +353 -0
- package/src/cli/doctor.js +548 -0
- package/src/cli/fbs.js +171 -0
- package/src/cli/finalise.js +377 -0
- package/src/cli/guidance.js +206 -0
- package/src/cli/help.js +156 -0
- package/src/cli/impact.js +119 -0
- package/src/cli/init.js +282 -0
- package/src/cli/intake.js +153 -0
- package/src/cli/link.js +128 -0
- package/src/cli/mcp.js +160 -0
- package/src/cli/preflight.js +220 -0
- package/src/cli/read.js +162 -0
- package/src/cli/req-baseline.js +269 -0
- package/src/cli/req-classify.js +135 -0
- package/src/cli/review.js +295 -0
- package/src/cli/test-suite.js +216 -0
- package/src/cli/trace.js +172 -0
- package/src/cli/ui-baseline.js +292 -0
- package/src/cli/ui-classify.js +108 -0
- package/src/cli/update.js +197 -0
- package/src/cli/validate.js +168 -0
- package/src/cli/view.js +452 -0
- package/src/core/baseline-catalog/data/auth.json +42 -0
- package/src/core/baseline-catalog/data/http-api.json +42 -0
- package/src/core/baseline-catalog/data/notifications.json +33 -0
- package/src/core/baseline-catalog/data/persistence.json +33 -0
- package/src/core/baseline-catalog/data/web-ui.json +60 -0
- package/src/core/baseline-catalog/index.js +121 -0
- package/src/core/errors/index.js +167 -0
- package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
- package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
- package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
- package/src/core/isolation/index.js +60 -0
- package/src/core/mcp/framing.js +103 -0
- package/src/core/mcp/index.js +8 -0
- package/src/core/mcp/server.js +228 -0
- package/src/core/patterns/register-canary.js +209 -0
- package/src/core/patterns/req-shapes.js +158 -0
- package/src/core/patterns/services.js +358 -0
- package/src/core/patterns/ui-shapes.js +166 -0
- package/src/core/store/cn-resolve.js +134 -0
- package/src/core/store/derive-deps.js +93 -0
- package/src/core/store/ids.js +78 -0
- package/src/core/store/index.js +20 -0
- package/src/core/store/init.js +255 -0
- package/src/core/store/loader.js +211 -0
- package/src/core/store/tp-resolve.js +176 -0
- package/src/core/store/validator.js +191 -0
- package/src/core/store/walker.js +898 -0
- package/src/core/store/writer.js +1849 -0
- package/src/design/index.js +11 -0
- package/src/design/writer.js +271 -0
- package/src/finalise/detect.js +129 -0
- package/src/finalise/index.js +17 -0
- package/src/finalise/ingest.js +119 -0
- package/src/finalise/install.js +119 -0
- package/src/finalise/ship-without-verified.js +131 -0
- package/src/finalise/spawn.js +84 -0
- package/src/intake/fidelity.js +105 -0
- package/src/intake/index.js +6 -0
- package/src/intake/manifest-writer.js +100 -0
- package/src/intake/orchestrator.js +138 -0
- package/src/intake/validate.js +80 -0
- package/src/mcp/map-errors.js +131 -0
- package/src/mcp/prompts.js +49 -0
- package/src/mcp/resources.js +244 -0
- package/src/mcp/tools.js +1204 -0
- package/src/preflight/design-shapes.js +185 -0
- package/src/preflight/index.js +90 -0
- package/src/preflight/manifest-writer.js +264 -0
- package/src/preflight/scanner.js +206 -0
- package/src/preflight/secrets.js +134 -0
- package/src/preflight/session.js +246 -0
- package/src/query/attestation.js +285 -0
- package/src/query/coverage.js +308 -0
- package/src/query/formatters/json.js +21 -0
- package/src/query/formatters/mermaid.js +209 -0
- package/src/query/formatters/table.js +203 -0
- package/src/query/impact.js +173 -0
- package/src/query/index.js +9 -0
- package/src/query/trace.js +345 -0
- package/src/register-canary/fixture-loader.js +87 -0
- package/src/register-canary/index.js +10 -0
- package/src/register-canary/record-writer.js +132 -0
- package/src/register-canary/runner.js +156 -0
- package/src/req-baseline/gate.js +86 -0
- package/src/req-baseline/index.js +27 -0
- package/src/req-baseline/open-candidates.js +143 -0
- package/src/req-baseline/opt-out.js +195 -0
- package/src/req-baseline/sweep.js +230 -0
- package/src/req-detection/classifier.js +181 -0
- package/src/req-detection/index.js +9 -0
- package/src/req-detection/persist.js +55 -0
- package/src/review/index.js +325 -0
- package/src/review/mutation.js +117 -0
- package/src/review/ui-baseline-drift.js +138 -0
- package/src/server/index.js +178 -0
- package/src/server/routes.js +110 -0
- package/src/server/sse.js +118 -0
- package/src/setup/agent-setup.js +362 -0
- package/src/setup/identity-seed.js +104 -0
- package/src/setup/knowledge-seed.js +123 -0
- package/src/setup/managed-block.js +193 -0
- package/src/setup/managed-gitignore.js +166 -0
- package/src/setup/managed-markers.js +49 -0
- package/src/ui-baseline/defaults.js +119 -0
- package/src/ui-baseline/index.js +25 -0
- package/src/ui-baseline/manifest-writer.js +282 -0
- package/src/ui-baseline/session.js +178 -0
- package/src/ui-detection/classifier.js +192 -0
- package/src/verify/chain/index.js +190 -0
- package/src/verify/cli/cleanup.js +61 -0
- package/src/verify/cli/help.js +56 -0
- package/src/verify/cli/mcp.js +98 -0
- package/src/verify/cli/provision.js +71 -0
- package/src/verify/cli/report.js +71 -0
- package/src/verify/cli/run.js +155 -0
- package/src/verify/engine/brief.js +87 -0
- package/src/verify/engine/index.js +177 -0
- package/src/verify/engine/launcher.js +307 -0
- package/src/verify/mcp/tools.js +107 -0
- package/src/verify/profile/index.js +146 -0
- package/src/verify/provision/index.js +256 -0
- package/src/verify/report/index.js +139 -0
- package/src/verify/report/renderer.js +118 -0
- package/src/verify/verdict/index.js +246 -0
- package/src/view/doc-renderers/adr.js +44 -0
- package/src/view/doc-renderers/build-sequence.js +40 -0
- package/src/view/doc-renderers/fbs.js +128 -0
- package/src/view/doc-renderers/helpers.js +159 -0
- package/src/view/doc-renderers/index.js +12 -0
- package/src/view/doc-renderers/prd.js +45 -0
- package/src/view/doc-renderers/req.js +43 -0
- package/src/view/doc-renderers/tac.js +38 -0
- package/src/view/doc-renderers/tad.js +74 -0
- package/src/view/doc-renderers/test-suite.js +45 -0
- package/src/view/doc-renderers/user-story.js +63 -0
- package/src/view/html-page.js +462 -0
- package/src/view/index.js +63 -0
- package/src/view/live-client.js +338 -0
- package/src/view/mermaid-diagram.js +178 -0
- package/src/view/style.css +735 -0
- package/src/view/tree-model.js +152 -0
- package/src/view/vendored/mermaid.min.js +2607 -0
- package/src/view-supervisor/index.js +26 -0
- package/src/view-supervisor/logs.js +32 -0
- package/src/view-supervisor/manifest-writer.js +178 -0
- package/src/view-supervisor/persist-until.js +85 -0
- package/src/view-supervisor/supervisor.js +276 -0
- package/src/watch/index.js +152 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// Assembles index.html from doc-renderer output and the per-REQ Mermaid
|
|
2
|
+
// subdiagrams. Phase 3.2 introduced the four-tab layout (Overview,
|
|
3
|
+
// Requirements, Architecture, Build sequence) plus nested `<details>`
|
|
4
|
+
// drill-down under Requirements. Phase 3.6 dropped the top-of-overview
|
|
5
|
+
// diagram - it was redundant with the PRD body's requirementIds list
|
|
6
|
+
// and unwieldy past ~15 REQs. Overview tab now renders the PRD body
|
|
7
|
+
// only. Client-side tabs + hash routing are wired by an inline script
|
|
8
|
+
// at the end of `<body>`; if JS is unavailable, every tabpanel is
|
|
9
|
+
// visible in DOM order (D12).
|
|
10
|
+
//
|
|
11
|
+
// Phase 3.8 wraps the swappable tree content in a stable
|
|
12
|
+
// `<div id="rcf-live-content">` (D13a) and always injects the live
|
|
13
|
+
// client script `<script src="/live-client.js" defer>` before `</body>`.
|
|
14
|
+
// The tab init routine is exposed as `window.rcfPage.init()` so the
|
|
15
|
+
// live client can re-invoke it after every SSE innerHTML swap.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
renderAdr,
|
|
19
|
+
renderBuildSequence,
|
|
20
|
+
renderFbs,
|
|
21
|
+
renderPrd,
|
|
22
|
+
renderReq,
|
|
23
|
+
renderTac,
|
|
24
|
+
renderTad,
|
|
25
|
+
renderTestSuite,
|
|
26
|
+
renderUserStory,
|
|
27
|
+
} from './doc-renderers/index.js';
|
|
28
|
+
import { detailsWrap, escapeHtml } from './doc-renderers/helpers.js';
|
|
29
|
+
import { allRequirementSubdiagrams } from './mermaid-diagram.js';
|
|
30
|
+
|
|
31
|
+
// Inline SVG favicon: the Stravica monogram (terracotta rounded-square with
|
|
32
|
+
// cream serif S), pinched from stravica.ai/assets/brand/logo-monogram.svg
|
|
33
|
+
// so the review surface shares brand identity with the marketing site.
|
|
34
|
+
// Delivered as a data URL so no separate file has to ship.
|
|
35
|
+
const FAVICON_HREF =
|
|
36
|
+
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Crect width='512' height='512' rx='64' fill='%23c14a3a'/%3E%3Ctext x='256' y='370' font-family='Georgia,serif' font-size='360' font-weight='600' text-anchor='middle' fill='%23f7f5f0'%3ES%3C/text%3E%3C/svg%3E";
|
|
37
|
+
|
|
38
|
+
const LIVE_WRAPPER_OPEN = '<div id="rcf-live-content">';
|
|
39
|
+
const LIVE_WRAPPER_CLOSE = '</div>';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Render the complete index.html string. Phase 3.8: always includes the
|
|
43
|
+
* live-content wrapper and the live-client script tag.
|
|
44
|
+
*
|
|
45
|
+
* @param {import('./tree-model.js').BuiltTreeModel} model
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function renderPage(model) {
|
|
49
|
+
const projectName = model.manifest?.projectName ?? model.prd?.productName ?? 'RCF project';
|
|
50
|
+
const contentHtml = renderContent(model);
|
|
51
|
+
const script = inlineScript();
|
|
52
|
+
|
|
53
|
+
return `<!DOCTYPE html>
|
|
54
|
+
<html lang="en-GB">
|
|
55
|
+
<head>
|
|
56
|
+
<meta charset="utf-8">
|
|
57
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
58
|
+
<title>${escapeHtml(projectName)} - RCF review surface</title>
|
|
59
|
+
<link rel="icon" type="image/svg+xml" href="${FAVICON_HREF}">
|
|
60
|
+
<link rel="stylesheet" href="style.css">
|
|
61
|
+
<noscript>
|
|
62
|
+
<style>
|
|
63
|
+
/* Progressive enhancement per D12: no JS -> every tabpanel is
|
|
64
|
+
visible, stacked in DOM order. The tab bar becomes visual chrome
|
|
65
|
+
only (buttons do nothing) so we hide it. */
|
|
66
|
+
nav.tabs { display: none !important; }
|
|
67
|
+
section[role="tabpanel"][hidden] { display: block !important; }
|
|
68
|
+
</style>
|
|
69
|
+
</noscript>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<header>
|
|
73
|
+
<h1>${escapeHtml(projectName)}</h1>
|
|
74
|
+
<p class="subtitle">RCF review surface</p>
|
|
75
|
+
<nav class="tabs" role="tablist" aria-label="Document sections">
|
|
76
|
+
<button type="button" role="tab" data-tab="overview" aria-selected="true" aria-controls="tab-overview">Overview</button>
|
|
77
|
+
<button type="button" role="tab" data-tab="requirements" aria-selected="false" aria-controls="tab-requirements">Requirements</button>
|
|
78
|
+
<button type="button" role="tab" data-tab="architecture" aria-selected="false" aria-controls="tab-architecture">Architecture</button>
|
|
79
|
+
<button type="button" role="tab" data-tab="build" aria-selected="false" aria-controls="tab-build">Build sequence</button>
|
|
80
|
+
</nav>
|
|
81
|
+
</header>
|
|
82
|
+
<main>
|
|
83
|
+
${LIVE_WRAPPER_OPEN}
|
|
84
|
+
${contentHtml}
|
|
85
|
+
${LIVE_WRAPPER_CLOSE}
|
|
86
|
+
</main>
|
|
87
|
+
<footer>
|
|
88
|
+
<p>Generated from the on-disk RCF tree at <code>rcf/</code>. Read-only; changes on disk stream to this tab automatically.</p>
|
|
89
|
+
<p>Learn more about the Requirements Confidence Framework at <a href="https://stravica.ai/rcf-methodology" target="_blank" rel="noopener">stravica.ai/rcf-methodology</a>.</p>
|
|
90
|
+
</footer>
|
|
91
|
+
<script src="mermaid.min.js"></script>
|
|
92
|
+
<script>${script}</script>
|
|
93
|
+
<script src="/live-client.js" defer></script>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Render the innerHTML of the swappable `<div id="rcf-live-content">`
|
|
101
|
+
* container - i.e. everything inside `<main>`. This is the payload the
|
|
102
|
+
* SSE `tree-update` event carries; the live client replaces the
|
|
103
|
+
* wrapper's innerHTML with this string.
|
|
104
|
+
*
|
|
105
|
+
* @param {import('./tree-model.js').BuiltTreeModel} model
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
export function renderContent(model) {
|
|
109
|
+
const subdiagrams = allRequirementSubdiagrams(model);
|
|
110
|
+
|
|
111
|
+
const prdSection = model.prd
|
|
112
|
+
? renderPrd(model.prd, {
|
|
113
|
+
raw: model.rawById.get(model.prd.prdId),
|
|
114
|
+
errors: model.errorsById.get(model.prd.prdId),
|
|
115
|
+
requirementIds: model.childrenByParent.get(model.prd.prdId) ?? [],
|
|
116
|
+
})
|
|
117
|
+
: '<p><em>No PRD on disk.</em></p>';
|
|
118
|
+
|
|
119
|
+
const requirementsPanel = renderRequirementsPanel(model, subdiagrams);
|
|
120
|
+
const architecturePanel = renderArchitecturePanel(model);
|
|
121
|
+
const buildPanel = renderBuildPanel(model);
|
|
122
|
+
|
|
123
|
+
const errorBanner = renderErrorBanner(model.errors ?? []);
|
|
124
|
+
|
|
125
|
+
return `${errorBanner}
|
|
126
|
+
<section id="tab-overview" role="tabpanel" aria-labelledby="tab-overview-button">
|
|
127
|
+
<h2 class="tab-heading">Overview</h2>
|
|
128
|
+
<div class="prd-body">
|
|
129
|
+
${prdSection}
|
|
130
|
+
</div>
|
|
131
|
+
</section>
|
|
132
|
+
<section id="tab-requirements" role="tabpanel" hidden>
|
|
133
|
+
<h2 class="tab-heading">Requirements</h2>
|
|
134
|
+
${requirementsPanel}
|
|
135
|
+
</section>
|
|
136
|
+
<section id="tab-architecture" role="tabpanel" hidden>
|
|
137
|
+
<h2 class="tab-heading">Architecture</h2>
|
|
138
|
+
${architecturePanel}
|
|
139
|
+
</section>
|
|
140
|
+
<section id="tab-build" role="tabpanel" hidden>
|
|
141
|
+
<h2 class="tab-heading">Build sequence</h2>
|
|
142
|
+
${buildPanel}
|
|
143
|
+
</section>`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderRequirementsPanel(model, subdiagrams) {
|
|
147
|
+
if (model.requirements.length === 0 && model.userStories.length === 0) {
|
|
148
|
+
return '<p><em>No requirements on disk.</em></p>';
|
|
149
|
+
}
|
|
150
|
+
const reqBlocks = model.requirements.map((r) => {
|
|
151
|
+
const reqBody = renderReq(r, {
|
|
152
|
+
raw: model.rawById.get(r.reqId),
|
|
153
|
+
errors: model.errorsById.get(r.reqId),
|
|
154
|
+
subdiagram: subdiagrams.get(r.reqId),
|
|
155
|
+
});
|
|
156
|
+
const stories = model.storiesByReqId.get(r.reqId) ?? [];
|
|
157
|
+
const usBlocks = stories.map((u) => detailsWrap({
|
|
158
|
+
id: u.usId,
|
|
159
|
+
summary: `${u.usId} - ${u.title ?? ''}`,
|
|
160
|
+
className: 'doc-us-wrap',
|
|
161
|
+
status: u.status,
|
|
162
|
+
body: renderUserStory(u, {
|
|
163
|
+
raw: model.rawById.get(u.usId),
|
|
164
|
+
errors: model.errorsById.get(u.usId),
|
|
165
|
+
fbsByAcId: model.fbsByAcId,
|
|
166
|
+
}),
|
|
167
|
+
})).join('\n');
|
|
168
|
+
const storiesSection = usBlocks
|
|
169
|
+
? `<section class="nested-details"><h4>User stories</h4>${usBlocks}</section>`
|
|
170
|
+
: '<p><em>No user stories under this requirement.</em></p>';
|
|
171
|
+
return detailsWrap({
|
|
172
|
+
id: r.reqId,
|
|
173
|
+
summary: `${r.reqId} - ${r.title ?? ''}`,
|
|
174
|
+
className: 'doc-req-wrap',
|
|
175
|
+
status: r.status,
|
|
176
|
+
body: `${reqBody}\n${storiesSection}`,
|
|
177
|
+
});
|
|
178
|
+
}).join('\n');
|
|
179
|
+
|
|
180
|
+
const orphanUs = model.userStories.filter((u) => !u.reqId || !model.requirements.some((r) => r.reqId === u.reqId));
|
|
181
|
+
const orphanBlock = orphanUs.length > 0
|
|
182
|
+
? `<section class="orphan-us"><h3>Orphan user stories</h3>${orphanUs.map((u) => detailsWrap({
|
|
183
|
+
id: u.usId,
|
|
184
|
+
summary: `${u.usId} - ${u.title ?? ''}`,
|
|
185
|
+
className: 'doc-us-wrap',
|
|
186
|
+
status: u.status,
|
|
187
|
+
body: renderUserStory(u, {
|
|
188
|
+
raw: model.rawById.get(u.usId),
|
|
189
|
+
errors: model.errorsById.get(u.usId),
|
|
190
|
+
fbsByAcId: model.fbsByAcId,
|
|
191
|
+
}),
|
|
192
|
+
})).join('\n')}</section>`
|
|
193
|
+
: '';
|
|
194
|
+
|
|
195
|
+
return `${reqBlocks}\n${orphanBlock}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderArchitecturePanel(model) {
|
|
199
|
+
const tadChildren = model.tad ? (model.childrenByParent.get(model.tad.tadId) ?? []) : [];
|
|
200
|
+
const componentIds = tadChildren.filter((id) => id.startsWith('TAC-'));
|
|
201
|
+
const architecturalDecisionIds = tadChildren.filter((id) => id.startsWith('ADR-'));
|
|
202
|
+
const tadSection = model.tad
|
|
203
|
+
? renderTad(model.tad, {
|
|
204
|
+
raw: model.rawById.get(model.tad.tadId),
|
|
205
|
+
errors: model.errorsById.get(model.tad.tadId),
|
|
206
|
+
componentIds,
|
|
207
|
+
architecturalDecisionIds,
|
|
208
|
+
})
|
|
209
|
+
: '<p><em>No TAD on disk.</em></p>';
|
|
210
|
+
|
|
211
|
+
const tacBlocks = model.tacs.map((t) => detailsWrap({
|
|
212
|
+
id: t.tacId,
|
|
213
|
+
summary: `${t.tacId} - ${t.name ?? ''}`,
|
|
214
|
+
className: 'doc-tac-wrap',
|
|
215
|
+
status: t.status,
|
|
216
|
+
body: renderTac(t, {
|
|
217
|
+
raw: model.rawById.get(t.tacId),
|
|
218
|
+
errors: model.errorsById.get(t.tacId),
|
|
219
|
+
}),
|
|
220
|
+
})).join('\n');
|
|
221
|
+
|
|
222
|
+
const adrBlocks = model.adrs.map((a) => detailsWrap({
|
|
223
|
+
id: a.adrId,
|
|
224
|
+
summary: `${a.adrId} - ${a.title ?? ''}`,
|
|
225
|
+
className: 'doc-adr-wrap',
|
|
226
|
+
status: a.status,
|
|
227
|
+
body: renderAdr(a, {
|
|
228
|
+
raw: model.rawById.get(a.adrId),
|
|
229
|
+
errors: model.errorsById.get(a.adrId),
|
|
230
|
+
}),
|
|
231
|
+
})).join('\n');
|
|
232
|
+
|
|
233
|
+
return `
|
|
234
|
+
${tadSection}
|
|
235
|
+
<h3 class="group-heading">Components</h3>
|
|
236
|
+
${tacBlocks || '<p><em>No TAC components on disk.</em></p>'}
|
|
237
|
+
<h3 class="group-heading">Architectural decisions</h3>
|
|
238
|
+
${adrBlocks || '<p><em>No ADRs on disk.</em></p>'}
|
|
239
|
+
`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderBuildPanel(model) {
|
|
243
|
+
const bsSlots = model.bs
|
|
244
|
+
? [...model.fbsItems]
|
|
245
|
+
.filter((f) => f.bsId === model.bs.bsId)
|
|
246
|
+
.sort((a, b) => (a.buildOrder ?? 0) - (b.buildOrder ?? 0))
|
|
247
|
+
.map((f) => ({
|
|
248
|
+
fbsId: f.fbsId,
|
|
249
|
+
buildOrder: f.buildOrder,
|
|
250
|
+
executionStatus: f.executionStatus,
|
|
251
|
+
title: f.title,
|
|
252
|
+
}))
|
|
253
|
+
: [];
|
|
254
|
+
const bsSection = model.bs
|
|
255
|
+
? renderBuildSequence(model.bs, {
|
|
256
|
+
raw: model.rawById.get(model.bs.bsId),
|
|
257
|
+
errors: model.errorsById.get(model.bs.bsId),
|
|
258
|
+
slots: bsSlots,
|
|
259
|
+
})
|
|
260
|
+
: '<p><em>No build sequence on disk.</em></p>';
|
|
261
|
+
|
|
262
|
+
const fbsBlocks = model.fbsItems.map((f) => detailsWrap({
|
|
263
|
+
id: f.fbsId,
|
|
264
|
+
summary: `${f.fbsId} - ${f.title ?? ''}`,
|
|
265
|
+
className: 'doc-fbs-wrap',
|
|
266
|
+
status: f.executionStatus,
|
|
267
|
+
body: renderFbs(f, {
|
|
268
|
+
raw: model.rawById.get(f.fbsId),
|
|
269
|
+
errors: model.errorsById.get(f.fbsId),
|
|
270
|
+
usByAcId: model.usByAcId,
|
|
271
|
+
}),
|
|
272
|
+
})).join('\n');
|
|
273
|
+
|
|
274
|
+
const tsBlocks = model.testSuites.map((ts) => detailsWrap({
|
|
275
|
+
id: ts.id,
|
|
276
|
+
summary: `${ts.id} - ${ts.title ?? 'test suite'}`,
|
|
277
|
+
className: 'doc-ts-wrap',
|
|
278
|
+
status: ts.status,
|
|
279
|
+
body: renderTestSuite(ts, {
|
|
280
|
+
raw: model.rawById.get(ts.id),
|
|
281
|
+
errors: model.errorsById.get(ts.id),
|
|
282
|
+
}),
|
|
283
|
+
})).join('\n');
|
|
284
|
+
|
|
285
|
+
const tsSection = tsBlocks
|
|
286
|
+
? `<h3 class="group-heading">Test suites</h3>${tsBlocks}`
|
|
287
|
+
: '';
|
|
288
|
+
|
|
289
|
+
return `
|
|
290
|
+
${bsSection}
|
|
291
|
+
<h3 class="group-heading">Functional Build Specifications</h3>
|
|
292
|
+
${fbsBlocks || '<p><em>No FBS items on disk.</em></p>'}
|
|
293
|
+
${tsSection}
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function renderErrorBanner(errors) {
|
|
298
|
+
if (!errors || errors.length === 0) return '';
|
|
299
|
+
const count = errors.length;
|
|
300
|
+
const noun = count === 1 ? 'error' : 'errors';
|
|
301
|
+
const items = errors.slice(0, 20).map((e) => {
|
|
302
|
+
const label = e.documentId ? `${e.documentId}` : e.filePath ?? '';
|
|
303
|
+
return `<li><strong>${escapeHtml(e.kind)}</strong> ${escapeHtml(label)} - ${escapeHtml(e.message)}</li>`;
|
|
304
|
+
}).join('');
|
|
305
|
+
const more = count > 20 ? `<p><em>... and ${count - 20} more</em></p>` : '';
|
|
306
|
+
return `<aside class="tree-errors" role="alert">
|
|
307
|
+
<h2>Tree has ${count} ${noun}</h2>
|
|
308
|
+
<ul>${items}</ul>
|
|
309
|
+
${more}
|
|
310
|
+
</aside>`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Inline client-side script: initialises Mermaid, wires tab switching,
|
|
314
|
+
// resolves hash routes to a tab + opens any ancestor `<details>` on the
|
|
315
|
+
// target. Vanilla; no dependencies beyond the vendored Mermaid loaded above.
|
|
316
|
+
// Phase 3.8 note: this script is byte-identical to the Phase 3.6 shape
|
|
317
|
+
// (the layout-regression test guards it). Live-swap recovery lives
|
|
318
|
+
// entirely in `src/view/live-client.js`, which reads state from the DOM
|
|
319
|
+
// after each SSE innerHTML swap - it does not call into this IIFE.
|
|
320
|
+
function inlineScript() {
|
|
321
|
+
return `
|
|
322
|
+
(function () {
|
|
323
|
+
var TABS = ['overview', 'requirements', 'architecture', 'build'];
|
|
324
|
+
|
|
325
|
+
function initMermaid() {
|
|
326
|
+
if (typeof window.mermaid !== 'undefined') {
|
|
327
|
+
// startOnLoad: false. Mermaid can't lay out diagrams inside a
|
|
328
|
+
// display:none tabpanel (getBoundingClientRect is 0x0 -> NaN
|
|
329
|
+
// transforms). We run each tab's diagrams on activation instead;
|
|
330
|
+
// runMermaidIn is idempotent via the data-processed attribute.
|
|
331
|
+
window.mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function runMermaidIn(container) {
|
|
336
|
+
if (!container || typeof window.mermaid === 'undefined') return;
|
|
337
|
+
var pending = container.querySelectorAll('.mermaid:not([data-processed="true"])');
|
|
338
|
+
if (pending.length === 0) return;
|
|
339
|
+
try {
|
|
340
|
+
window.mermaid.run({ nodes: Array.prototype.slice.call(pending) });
|
|
341
|
+
} catch (e) {
|
|
342
|
+
// Mermaid.run may throw on init errors; do not block the UI.
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function tabButtons() {
|
|
347
|
+
return Array.prototype.slice.call(document.querySelectorAll('[role="tab"]'));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function panelFor(name) {
|
|
351
|
+
return document.getElementById('tab-' + name);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function activateTab(name) {
|
|
355
|
+
if (TABS.indexOf(name) === -1) return false;
|
|
356
|
+
tabButtons().forEach(function (btn) {
|
|
357
|
+
var isTarget = btn.getAttribute('data-tab') === name;
|
|
358
|
+
btn.setAttribute('aria-selected', isTarget ? 'true' : 'false');
|
|
359
|
+
});
|
|
360
|
+
TABS.forEach(function (t) {
|
|
361
|
+
var p = panelFor(t);
|
|
362
|
+
if (!p) return;
|
|
363
|
+
if (t === name) {
|
|
364
|
+
p.removeAttribute('hidden');
|
|
365
|
+
runMermaidIn(p);
|
|
366
|
+
} else {
|
|
367
|
+
p.setAttribute('hidden', '');
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function tabForNode(node) {
|
|
374
|
+
var cur = node;
|
|
375
|
+
while (cur && cur !== document.body) {
|
|
376
|
+
if (cur.getAttribute && cur.getAttribute('role') === 'tabpanel') {
|
|
377
|
+
var id = cur.id || '';
|
|
378
|
+
if (id.indexOf('tab-') === 0) return id.slice(4);
|
|
379
|
+
}
|
|
380
|
+
cur = cur.parentNode;
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function openAncestorDetails(node) {
|
|
386
|
+
var cur = node.parentNode;
|
|
387
|
+
while (cur && cur !== document.body) {
|
|
388
|
+
if (cur.tagName && cur.tagName.toLowerCase() === 'details') {
|
|
389
|
+
cur.open = true;
|
|
390
|
+
}
|
|
391
|
+
cur = cur.parentNode;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function findByDocId(id) {
|
|
396
|
+
if (!id) return null;
|
|
397
|
+
var byId = document.getElementById(id);
|
|
398
|
+
if (byId) return byId;
|
|
399
|
+
return document.querySelector('[data-doc-id="' + id.replace(/"/g, '\\\\"') + '"]');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function resolveHash(hash) {
|
|
403
|
+
if (!hash) {
|
|
404
|
+
activateTab('overview');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
var raw = hash.charAt(0) === '#' ? hash.slice(1) : hash;
|
|
408
|
+
if (raw.indexOf('tab=') === 0) {
|
|
409
|
+
activateTab(raw.slice(4));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
var target = findByDocId(raw);
|
|
413
|
+
if (!target) {
|
|
414
|
+
activateTab('overview');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
var tab = tabForNode(target);
|
|
418
|
+
if (tab) activateTab(tab);
|
|
419
|
+
openAncestorDetails(target);
|
|
420
|
+
if (target.tagName && target.tagName.toLowerCase() === 'details') {
|
|
421
|
+
target.open = true;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
target.scrollIntoView({ block: 'start' });
|
|
425
|
+
} catch (e) {
|
|
426
|
+
target.scrollIntoView();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function onTabClick(ev) {
|
|
431
|
+
var btn = ev.currentTarget;
|
|
432
|
+
var name = btn.getAttribute('data-tab');
|
|
433
|
+
if (!name) return;
|
|
434
|
+
activateTab(name);
|
|
435
|
+
if (window.history && typeof window.history.replaceState === 'function') {
|
|
436
|
+
window.history.replaceState(null, '', '#tab=' + name);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function wireTabs() {
|
|
441
|
+
tabButtons().forEach(function (btn) {
|
|
442
|
+
btn.addEventListener('click', onTabClick);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function onReady() {
|
|
447
|
+
initMermaid();
|
|
448
|
+
wireTabs();
|
|
449
|
+
resolveHash(window.location.hash);
|
|
450
|
+
window.addEventListener('hashchange', function () {
|
|
451
|
+
resolveHash(window.location.hash);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (document.readyState === 'loading') {
|
|
456
|
+
document.addEventListener('DOMContentLoaded', onReady);
|
|
457
|
+
} else {
|
|
458
|
+
onReady();
|
|
459
|
+
}
|
|
460
|
+
})();
|
|
461
|
+
`.trim();
|
|
462
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// View layer entry point. Locates the project root, walks the tree via the
|
|
2
|
+
// store, builds the render model and hands the server the pre-rendered
|
|
3
|
+
// HTML strings. Phase 3.8 removed the disk-write path (`.rcf-view/`
|
|
4
|
+
// convention retired wholesale); the view surface is now server-only.
|
|
5
|
+
// See specs/phase-3.8-live-view.md D9 for the CLI rewrite rationale.
|
|
6
|
+
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { stat } from 'node:fs/promises';
|
|
10
|
+
|
|
11
|
+
import { walkTree } from '#core/store';
|
|
12
|
+
import { renderContent, renderPage } from './html-page.js';
|
|
13
|
+
import { buildTreeModel } from './tree-model.js';
|
|
14
|
+
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
export const VENDORED_MERMAID_PATH = resolve(here, 'vendored', 'mermaid.min.js');
|
|
17
|
+
export const STYLE_CSS_PATH = resolve(here, 'style.css');
|
|
18
|
+
export const LIVE_CLIENT_PATH = resolve(here, 'live-client.js');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Walk up from `start` looking for an ancestor directory containing a
|
|
22
|
+
* `rcf/manifest.json`. Returns the absolute directory path or null.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} start - absolute directory to start from
|
|
25
|
+
* @returns {Promise<string | null>}
|
|
26
|
+
*/
|
|
27
|
+
export async function findProjectRoot(start) {
|
|
28
|
+
let dir = resolve(start);
|
|
29
|
+
while (true) {
|
|
30
|
+
try {
|
|
31
|
+
const candidate = join(dir, 'rcf', 'manifest.json');
|
|
32
|
+
// eslint-disable-next-line no-await-in-loop
|
|
33
|
+
const s = await stat(candidate);
|
|
34
|
+
if (s.isFile()) return dir;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (/** @type {NodeJS.ErrnoException} */ (err).code !== 'ENOENT') throw err;
|
|
37
|
+
}
|
|
38
|
+
const parent = dirname(dir);
|
|
39
|
+
if (parent === dir) return null;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Walk the tree at `projectRoot`, build the render model and render both
|
|
46
|
+
* the full HTML page and the innerHTML of the swappable content wrapper.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} args
|
|
49
|
+
* @param {string} args.projectRoot - absolute path to the project root
|
|
50
|
+
* @returns {Promise<{
|
|
51
|
+
* fullPageHtml: string,
|
|
52
|
+
* contentHtml: string,
|
|
53
|
+
* errors: import('#core/errors').RcfError[],
|
|
54
|
+
* tree: import('#core/store/walker.js').Tree,
|
|
55
|
+
* }>}
|
|
56
|
+
*/
|
|
57
|
+
export async function renderModelToPage({ projectRoot }) {
|
|
58
|
+
const { tree, errors } = await walkTree({ projectRoot });
|
|
59
|
+
const model = buildTreeModel({ tree, errors });
|
|
60
|
+
const fullPageHtml = renderPage(model);
|
|
61
|
+
const contentHtml = renderContent(model);
|
|
62
|
+
return { fullPageHtml, contentHtml, errors, tree };
|
|
63
|
+
}
|