@laitszkin/apollo-toolkit 3.9.7 → 3.11.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.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +37 -0
- package/README.md +6 -0
- package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
- package/cjk-pdf/agents/openai.yaml +5 -0
- package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
- package/generate-spec/SKILL.md +26 -4
- package/generate-spec/agents/openai.yaml +1 -0
- package/generate-spec/references/TEMPLATE_SPEC.md +117 -0
- package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
- package/init-project-html/SKILL.md +137 -0
- package/init-project-html/agents/openai.yaml +22 -0
- package/init-project-html/lib/atlas/assets/architecture.css +140 -0
- package/init-project-html/lib/atlas/assets/viewer.client.js +93 -0
- package/init-project-html/lib/atlas/cli.js +995 -0
- package/init-project-html/lib/atlas/layout.js +229 -0
- package/init-project-html/lib/atlas/render.js +485 -0
- package/init-project-html/lib/atlas/schema.js +310 -0
- package/init-project-html/lib/atlas/state.js +402 -0
- package/init-project-html/references/TEMPLATE_SPEC.md +137 -0
- package/init-project-html/references/architecture-page.template.html +35 -0
- package/init-project-html/references/architecture.css +1059 -0
- package/init-project-html/sample-demo/resources/project-architecture/assets/architecture.css +140 -0
- package/init-project-html/sample-demo/resources/project-architecture/assets/viewer.client.js +93 -0
- package/init-project-html/sample-demo/resources/project-architecture/atlas/atlas.index.yaml +34 -0
- package/init-project-html/sample-demo/resources/project-architecture/atlas/features/get-invite-codes.yaml +159 -0
- package/init-project-html/sample-demo/resources/project-architecture/atlas/features/invite-code-registration.yaml +160 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/index.html +69 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-code-generator.html +50 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-issuance-service.html +72 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/postgresql.html +66 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/public-api.html +70 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/web-get-invite-ui.html +67 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/index.html +63 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/postgresql.html +68 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/public-api.html +65 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/registration-service.html +79 -0
- package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/web-register-ui.html +67 -0
- package/init-project-html/sample-demo/resources/project-architecture/index.html +234 -0
- package/init-project-html/scripts/architecture.js +314 -0
- package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
- package/lib/cli.js +2 -0
- package/lib/tool-runner.js +7 -0
- package/merge-conflict-resolver/agents/openai.yaml +5 -0
- package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
- package/package.json +6 -2
- package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
- package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
- package/spec-to-project-html/SKILL.md +114 -0
- package/spec-to-project-html/agents/openai.yaml +18 -0
- package/spec-to-project-html/references/TEMPLATE_SPEC.md +111 -0
- package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* architecture.css — styling for the declarative atlas. The CLI copies
|
|
2
|
+
* this file into <outDir>/assets/. Style hooks (class names) are owned
|
|
3
|
+
* by render.js so agents never need to touch HTML by hand. */
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
color-scheme: light dark;
|
|
7
|
+
--bg: #0f172a;
|
|
8
|
+
--panel: #111827;
|
|
9
|
+
--panel-soft: #1f2937;
|
|
10
|
+
--text: #e5e7eb;
|
|
11
|
+
--muted: #9ca3af;
|
|
12
|
+
--border: #334155;
|
|
13
|
+
--accent: #38bdf8;
|
|
14
|
+
--kind-ui: #38bdf8;
|
|
15
|
+
--kind-api: #818cf8;
|
|
16
|
+
--kind-service: #34d399;
|
|
17
|
+
--kind-db: #fbbf24;
|
|
18
|
+
--kind-pure-fn: #cbd5e1;
|
|
19
|
+
--kind-queue: #a78bfa;
|
|
20
|
+
--kind-external: #fb7185;
|
|
21
|
+
--edge-call: #60a5fa;
|
|
22
|
+
--edge-return: #94a3b8;
|
|
23
|
+
--edge-data-row: #fbbf24;
|
|
24
|
+
--edge-failure: #fb7185;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
* { box-sizing: border-box; }
|
|
28
|
+
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
29
|
+
|
|
30
|
+
a { color: var(--accent); text-decoration: none; }
|
|
31
|
+
a:hover { text-decoration: underline; }
|
|
32
|
+
|
|
33
|
+
h1, h2 { margin: 0 0 12px; font-weight: 600; letter-spacing: -0.01em; }
|
|
34
|
+
h1 { font-size: 28px; }
|
|
35
|
+
h2 { font-size: 18px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; font-size: 13px; }
|
|
36
|
+
|
|
37
|
+
p { line-height: 1.55; color: var(--text); }
|
|
38
|
+
|
|
39
|
+
/* ---- atlas (macro) ---- */
|
|
40
|
+
.atlas-header { padding: 28px 40px 16px; border-bottom: 1px solid var(--border); background: var(--panel); }
|
|
41
|
+
.atlas-summary { color: var(--muted); max-width: 80ch; margin: 8px 0 0; }
|
|
42
|
+
|
|
43
|
+
.atlas-main { display: grid; grid-template-columns: minmax(0, 3fr) minmax(280px, 1fr); gap: 24px; padding: 24px 40px 48px; }
|
|
44
|
+
|
|
45
|
+
.atlas-canvas { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; position: relative; }
|
|
46
|
+
.atlas-canvas__toolbar { position: absolute; top: 16px; right: 16px; display: flex; gap: 4px; z-index: 2; }
|
|
47
|
+
.atlas-canvas__toolbar button { background: var(--panel-soft); color: var(--text); border: 1px solid var(--border); padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
|
48
|
+
.atlas-canvas__toolbar button:hover { border-color: var(--accent); color: var(--accent); }
|
|
49
|
+
|
|
50
|
+
.atlas-canvas__viewport { width: 100%; max-height: 72vh; overflow: hidden; border-radius: 8px; background: #0b1220; }
|
|
51
|
+
.atlas-canvas__viewport.is-grabbing { cursor: grabbing; }
|
|
52
|
+
.atlas-canvas__viewport:not(.is-grabbing) { cursor: grab; }
|
|
53
|
+
|
|
54
|
+
.atlas-svg { width: 100%; height: auto; max-height: 72vh; display: block; user-select: none; touch-action: none; }
|
|
55
|
+
|
|
56
|
+
.atlas-legend { list-style: none; padding: 12px 4px 0; margin: 0; display: flex; gap: 18px; flex-wrap: wrap; font-size: 12px; color: var(--muted); }
|
|
57
|
+
.atlas-legend li { display: inline-flex; align-items: center; gap: 6px; }
|
|
58
|
+
.legend-swatch { display: inline-block; width: 18px; height: 4px; border-radius: 2px; }
|
|
59
|
+
.legend-swatch--call { background: var(--edge-call); }
|
|
60
|
+
.legend-swatch--return { background: var(--edge-return); }
|
|
61
|
+
.legend-swatch--data-row { background: var(--edge-data-row); }
|
|
62
|
+
.legend-swatch--failure { background: var(--edge-failure); }
|
|
63
|
+
|
|
64
|
+
.atlas-index { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; max-height: 80vh; overflow: auto; }
|
|
65
|
+
|
|
66
|
+
.atlas-submodule-index { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; }
|
|
67
|
+
.atlas-submodule-index__item a { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px 8px; padding: 8px 10px; background: var(--panel-soft); border: 1px solid var(--border); border-radius: 8px; color: inherit; }
|
|
68
|
+
.atlas-submodule-index__item a:hover { border-color: var(--accent); }
|
|
69
|
+
.atlas-submodule-index__feature { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; grid-column: 1 / 3; }
|
|
70
|
+
.atlas-submodule-index__sub { font-weight: 600; }
|
|
71
|
+
.atlas-submodule-index__kind { font-size: 11px; padding: 1px 8px; border-radius: 999px; background: var(--panel); border: 1px solid var(--border); color: var(--muted); }
|
|
72
|
+
.atlas-submodule-index__role { margin: 4px 10px 0; font-size: 12px; color: var(--muted); }
|
|
73
|
+
|
|
74
|
+
/* ---- SVG macro ---- */
|
|
75
|
+
.m-cluster__bg { fill: rgba(15, 23, 42, 0.55); stroke: var(--border); stroke-width: 1; }
|
|
76
|
+
.m-cluster__title { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 14px; fill: var(--text); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
|
|
77
|
+
.m-node rect { fill: var(--panel-soft); stroke: var(--border); stroke-width: 1; transition: stroke 120ms ease; }
|
|
78
|
+
.m-node:hover rect { stroke: var(--accent); }
|
|
79
|
+
.m-node__title { font-size: 13px; font-weight: 600; fill: var(--text); }
|
|
80
|
+
.m-node__kind { font-size: 11px; fill: var(--muted); }
|
|
81
|
+
.m-node__role { font-size: 11px; fill: var(--muted); }
|
|
82
|
+
|
|
83
|
+
.m-node--ui rect { stroke: var(--kind-ui); }
|
|
84
|
+
.m-node--api rect { stroke: var(--kind-api); }
|
|
85
|
+
.m-node--service rect { stroke: var(--kind-service); }
|
|
86
|
+
.m-node--db rect { stroke: var(--kind-db); }
|
|
87
|
+
.m-node--pure-fn rect { stroke: var(--kind-pure-fn); }
|
|
88
|
+
.m-node--queue rect { stroke: var(--kind-queue); }
|
|
89
|
+
.m-node--external rect { stroke: var(--kind-external); }
|
|
90
|
+
|
|
91
|
+
.m-edge path { stroke-width: 1.6; }
|
|
92
|
+
.m-edge--call path { stroke: var(--edge-call); }
|
|
93
|
+
.m-edge--return path { stroke: var(--edge-return); stroke-dasharray: 6 4; }
|
|
94
|
+
.m-edge--data-row path { stroke: var(--edge-data-row); }
|
|
95
|
+
.m-edge--failure path { stroke: var(--edge-failure); }
|
|
96
|
+
|
|
97
|
+
.m-arrow path { fill: currentColor; }
|
|
98
|
+
.m-arrow--call { color: var(--edge-call); }
|
|
99
|
+
.m-arrow--return { color: var(--edge-return); }
|
|
100
|
+
.m-arrow--data-row { color: var(--edge-data-row); }
|
|
101
|
+
.m-arrow--failure { color: var(--edge-failure); }
|
|
102
|
+
|
|
103
|
+
.m-edge__label { fill: var(--muted); font-size: 11px; }
|
|
104
|
+
|
|
105
|
+
/* ---- feature page ---- */
|
|
106
|
+
.feature-header, .submodule-header { padding: 24px 40px 12px; border-bottom: 1px solid var(--border); background: var(--panel); }
|
|
107
|
+
.feature-breadcrumb, .submodule-breadcrumb { font-size: 13px; color: var(--muted); margin-bottom: 8px; }
|
|
108
|
+
.feature-depends { font-size: 13px; color: var(--muted); margin: 8px 0 0; }
|
|
109
|
+
.feature-main, .submodule-main { padding: 24px 40px 48px; display: flex; flex-direction: column; gap: 32px; }
|
|
110
|
+
.feature-story p { max-width: 80ch; }
|
|
111
|
+
|
|
112
|
+
.submodule-nav { list-style: none; padding: 0; margin: 0; display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
|
|
113
|
+
.submodule-card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; }
|
|
114
|
+
.submodule-card__link { display: flex; align-items: center; justify-content: space-between; color: inherit; font-weight: 600; }
|
|
115
|
+
.submodule-card__kind { font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); }
|
|
116
|
+
.submodule-card__role { margin: 8px 0 0; font-size: 13px; color: var(--muted); }
|
|
117
|
+
|
|
118
|
+
.feature-edges__list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
119
|
+
.feature-edges__item { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 2fr); gap: 8px; padding: 8px 12px; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; font-size: 13px; }
|
|
120
|
+
.feature-edges__kind { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; align-self: center; }
|
|
121
|
+
.feature-edges__item--call { border-left: 4px solid var(--edge-call); }
|
|
122
|
+
.feature-edges__item--return { border-left: 4px solid var(--edge-return); }
|
|
123
|
+
.feature-edges__item--data-row { border-left: 4px solid var(--edge-data-row); }
|
|
124
|
+
.feature-edges__item--failure { border-left: 4px solid var(--edge-failure); }
|
|
125
|
+
|
|
126
|
+
/* ---- submodule page ---- */
|
|
127
|
+
.submodule-kind { display: inline-block; font-size: 12px; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); margin-left: 8px; vertical-align: middle; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
128
|
+
.submodule-role { color: var(--muted); margin: 8px 0 0; max-width: 80ch; }
|
|
129
|
+
|
|
130
|
+
.sub-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
131
|
+
.sub-table th, .sub-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; }
|
|
132
|
+
.sub-table th { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; background: var(--panel); }
|
|
133
|
+
|
|
134
|
+
.sub-section__empty { color: var(--muted); font-style: italic; font-size: 13px; }
|
|
135
|
+
|
|
136
|
+
.sub-dataflow__svg { width: 100%; max-width: 480px; }
|
|
137
|
+
.sub-dataflow__step rect { fill: var(--panel); stroke: var(--border); }
|
|
138
|
+
.sub-dataflow__step text { fill: var(--text); font-size: 13px; }
|
|
139
|
+
.sub-dataflow__arrow { stroke: var(--muted); stroke-width: 1.4; }
|
|
140
|
+
.sub-dataflow__empty { color: var(--muted); font-style: italic; }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* viewer.client.js — pan/zoom for the macro atlas SVG. No deps; wires
|
|
2
|
+
* onto any element marked [data-pan-zoom-viewport] containing one
|
|
3
|
+
* [data-atlas-svg] SVG. Mouse wheel zooms around the cursor; drag pans;
|
|
4
|
+
* toolbar buttons handle +/-/Fit; keyboard arrows pan. */
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const viewport = document.querySelector('[data-pan-zoom-viewport]');
|
|
10
|
+
if (!viewport) return;
|
|
11
|
+
const svg = viewport.querySelector('[data-atlas-svg]');
|
|
12
|
+
if (!svg) return;
|
|
13
|
+
|
|
14
|
+
const initial = svg.getAttribute('viewBox');
|
|
15
|
+
if (!initial) return;
|
|
16
|
+
const [ix, iy, iw, ih] = initial.split(/\s+/).map(Number);
|
|
17
|
+
const state = { x: ix, y: iy, w: iw, h: ih };
|
|
18
|
+
|
|
19
|
+
function apply() {
|
|
20
|
+
svg.setAttribute('viewBox', `${state.x} ${state.y} ${state.w} ${state.h}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function zoom(factor, cx, cy) {
|
|
24
|
+
const newW = Math.max(40, Math.min(state.w * factor, iw * 8));
|
|
25
|
+
const newH = newW * (state.h / state.w);
|
|
26
|
+
if (cx == null) { cx = state.x + state.w / 2; cy = state.y + state.h / 2; }
|
|
27
|
+
state.x = cx - (cx - state.x) * (newW / state.w);
|
|
28
|
+
state.y = cy - (cy - state.y) * (newH / state.h);
|
|
29
|
+
state.w = newW;
|
|
30
|
+
state.h = newH;
|
|
31
|
+
apply();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function clientToSvg(evt) {
|
|
35
|
+
const rect = svg.getBoundingClientRect();
|
|
36
|
+
const xRatio = (evt.clientX - rect.left) / rect.width;
|
|
37
|
+
const yRatio = (evt.clientY - rect.top) / rect.height;
|
|
38
|
+
return { x: state.x + xRatio * state.w, y: state.y + yRatio * state.h };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
viewport.addEventListener('wheel', function (evt) {
|
|
42
|
+
if (!evt.ctrlKey && !evt.metaKey && Math.abs(evt.deltaY) < 4 && Math.abs(evt.deltaX) < 4) return;
|
|
43
|
+
evt.preventDefault();
|
|
44
|
+
const factor = evt.deltaY > 0 ? 1.1 : 1 / 1.1;
|
|
45
|
+
const pt = clientToSvg(evt);
|
|
46
|
+
zoom(factor, pt.x, pt.y);
|
|
47
|
+
}, { passive: false });
|
|
48
|
+
|
|
49
|
+
let dragging = null;
|
|
50
|
+
viewport.addEventListener('pointerdown', function (evt) {
|
|
51
|
+
if (evt.button !== 0) return;
|
|
52
|
+
dragging = { x: evt.clientX, y: evt.clientY };
|
|
53
|
+
viewport.classList.add('is-grabbing');
|
|
54
|
+
viewport.setPointerCapture(evt.pointerId);
|
|
55
|
+
});
|
|
56
|
+
viewport.addEventListener('pointermove', function (evt) {
|
|
57
|
+
if (!dragging) return;
|
|
58
|
+
const rect = svg.getBoundingClientRect();
|
|
59
|
+
const dx = ((evt.clientX - dragging.x) / rect.width) * state.w;
|
|
60
|
+
const dy = ((evt.clientY - dragging.y) / rect.height) * state.h;
|
|
61
|
+
state.x -= dx;
|
|
62
|
+
state.y -= dy;
|
|
63
|
+
dragging = { x: evt.clientX, y: evt.clientY };
|
|
64
|
+
apply();
|
|
65
|
+
});
|
|
66
|
+
function endDrag(evt) {
|
|
67
|
+
if (!dragging) return;
|
|
68
|
+
dragging = null;
|
|
69
|
+
viewport.classList.remove('is-grabbing');
|
|
70
|
+
try { viewport.releasePointerCapture(evt.pointerId); } catch (e) { /* ignore */ }
|
|
71
|
+
}
|
|
72
|
+
viewport.addEventListener('pointerup', endDrag);
|
|
73
|
+
viewport.addEventListener('pointercancel', endDrag);
|
|
74
|
+
viewport.addEventListener('pointerleave', endDrag);
|
|
75
|
+
|
|
76
|
+
document.addEventListener('keydown', function (evt) {
|
|
77
|
+
if (evt.target && (evt.target.tagName === 'INPUT' || evt.target.tagName === 'TEXTAREA')) return;
|
|
78
|
+
const step = state.w * 0.08;
|
|
79
|
+
if (evt.key === 'ArrowLeft') { state.x -= step; apply(); }
|
|
80
|
+
else if (evt.key === 'ArrowRight') { state.x += step; apply(); }
|
|
81
|
+
else if (evt.key === 'ArrowUp') { state.y -= step; apply(); }
|
|
82
|
+
else if (evt.key === 'ArrowDown') { state.y += step; apply(); }
|
|
83
|
+
else if (evt.key === '+' || evt.key === '=') { zoom(1 / 1.2); }
|
|
84
|
+
else if (evt.key === '-' || evt.key === '_') { zoom(1.2); }
|
|
85
|
+
else if (evt.key === '0') { state.x = ix; state.y = iy; state.w = iw; state.h = ih; apply(); }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
document.querySelectorAll('[data-pan-zoom="zoom-in"]').forEach((btn) => btn.addEventListener('click', () => zoom(1 / 1.2)));
|
|
89
|
+
document.querySelectorAll('[data-pan-zoom="zoom-out"]').forEach((btn) => btn.addEventListener('click', () => zoom(1.2)));
|
|
90
|
+
document.querySelectorAll('[data-pan-zoom="fit"]').forEach((btn) => btn.addEventListener('click', () => {
|
|
91
|
+
state.x = ix; state.y = iy; state.w = iw; state.h = ih; apply();
|
|
92
|
+
}));
|
|
93
|
+
})();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
meta:
|
|
2
|
+
title: Acme App — sample atlas
|
|
3
|
+
summary: >-
|
|
4
|
+
Two end-to-end feature modules — minting invite codes and consuming them during account
|
|
5
|
+
registration — produced by `apltk architecture` from a declarative YAML source. The macro
|
|
6
|
+
diagram surfaces both feature clusters and every sub-module they own, plus the cross-feature
|
|
7
|
+
data-row that carries an `invite_codes` row between them.
|
|
8
|
+
actors:
|
|
9
|
+
- id: visitor
|
|
10
|
+
label: Visitor (unregistered)
|
|
11
|
+
- id: member
|
|
12
|
+
label: Authenticated member
|
|
13
|
+
features:
|
|
14
|
+
- get-invite-codes
|
|
15
|
+
- invite-code-registration
|
|
16
|
+
edges:
|
|
17
|
+
- id: cross-issuance-to-postgres-codes
|
|
18
|
+
from:
|
|
19
|
+
feature: get-invite-codes
|
|
20
|
+
submodule: invite-issuance-service
|
|
21
|
+
to:
|
|
22
|
+
feature: get-invite-codes
|
|
23
|
+
submodule: postgresql
|
|
24
|
+
kind: data-row
|
|
25
|
+
label: INSERT invite_codes
|
|
26
|
+
- id: cross-postgres-codes-to-registration
|
|
27
|
+
from:
|
|
28
|
+
feature: get-invite-codes
|
|
29
|
+
submodule: postgresql
|
|
30
|
+
to:
|
|
31
|
+
feature: invite-code-registration
|
|
32
|
+
submodule: registration-service
|
|
33
|
+
kind: data-row
|
|
34
|
+
label: read/consume invite_codes
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
slug: get-invite-codes
|
|
2
|
+
title: Get invite codes
|
|
3
|
+
story: >-
|
|
4
|
+
An authenticated member requests a new invite code that they can hand to an off-platform
|
|
5
|
+
visitor. The issuance service mints a fresh code, persists it in `invite_codes`, and returns
|
|
6
|
+
the code string to the UI. Issued rows then become the producer side of the
|
|
7
|
+
`invite-code-registration` flow.
|
|
8
|
+
dependsOn: []
|
|
9
|
+
submodules:
|
|
10
|
+
- slug: web-get-invite-ui
|
|
11
|
+
kind: ui
|
|
12
|
+
role: React page that lets a signed-in member request a new invite code.
|
|
13
|
+
functions:
|
|
14
|
+
- name: handleGenerate
|
|
15
|
+
in: MouseEvent
|
|
16
|
+
out: void
|
|
17
|
+
side: io
|
|
18
|
+
purpose: Calls POST /api/invites and renders the returned code.
|
|
19
|
+
variables:
|
|
20
|
+
- name: userId
|
|
21
|
+
type: string
|
|
22
|
+
scope: call
|
|
23
|
+
purpose: Identifies the member who will own the new invite row.
|
|
24
|
+
- name: code
|
|
25
|
+
type: string
|
|
26
|
+
scope: instance
|
|
27
|
+
purpose: Last issued code shown to the member.
|
|
28
|
+
dataflow:
|
|
29
|
+
- Read userId from auth context.
|
|
30
|
+
- POST /api/invites with userId.
|
|
31
|
+
- Render returned code or surface error.
|
|
32
|
+
errors:
|
|
33
|
+
- name: ErrUnauthenticated
|
|
34
|
+
when: Session is missing on the client.
|
|
35
|
+
means: Redirect to sign-in.
|
|
36
|
+
- name: ErrIssuanceFailed
|
|
37
|
+
when: API returns 500.
|
|
38
|
+
means: Show retry banner.
|
|
39
|
+
- slug: public-api
|
|
40
|
+
kind: api
|
|
41
|
+
role: HTTP boundary for `/api/invites` POST requests.
|
|
42
|
+
functions:
|
|
43
|
+
- name: postInvites
|
|
44
|
+
in: HttpRequest
|
|
45
|
+
out: HttpResponse
|
|
46
|
+
side: network
|
|
47
|
+
purpose: Validates the bearer token then delegates to invite-issuance-service.
|
|
48
|
+
variables:
|
|
49
|
+
- name: token
|
|
50
|
+
type: string
|
|
51
|
+
scope: call
|
|
52
|
+
purpose: Bearer token used to resolve the requesting member.
|
|
53
|
+
dataflow:
|
|
54
|
+
- Validate Authorization header.
|
|
55
|
+
- Resolve userId from token.
|
|
56
|
+
- Call invite-issuance-service.Issue.
|
|
57
|
+
- Serialize response.
|
|
58
|
+
errors:
|
|
59
|
+
- name: ErrUnauthorized
|
|
60
|
+
when: Token missing or invalid.
|
|
61
|
+
means: 401 response.
|
|
62
|
+
- slug: invite-issuance-service
|
|
63
|
+
kind: service
|
|
64
|
+
role: Domain service that mints and persists a single invite row per request.
|
|
65
|
+
functions:
|
|
66
|
+
- name: Issue
|
|
67
|
+
in: ctx, userId
|
|
68
|
+
out: InviteCode | error
|
|
69
|
+
side: tx
|
|
70
|
+
purpose: Generates a unique code and writes the matching invite_codes row.
|
|
71
|
+
- name: generateCode
|
|
72
|
+
in: rand
|
|
73
|
+
out: code
|
|
74
|
+
side: pure
|
|
75
|
+
purpose: Produces a 10-char alphanumeric token.
|
|
76
|
+
variables:
|
|
77
|
+
- name: code
|
|
78
|
+
type: string
|
|
79
|
+
scope: tx
|
|
80
|
+
purpose: Newly minted invite token recorded against the member.
|
|
81
|
+
- name: row
|
|
82
|
+
type: InviteCodeRow
|
|
83
|
+
scope: tx
|
|
84
|
+
purpose: Persisted invite_codes row carrying owner, code, expiry, and consumption state.
|
|
85
|
+
dataflow:
|
|
86
|
+
- Open transaction.
|
|
87
|
+
- Generate candidate code via generateCode.
|
|
88
|
+
- INSERT invite_codes row (retry on unique-violation).
|
|
89
|
+
- Commit and return code.
|
|
90
|
+
errors:
|
|
91
|
+
- name: ErrCollision
|
|
92
|
+
when: Unique constraint on code repeatedly violated.
|
|
93
|
+
means: Surface 503 after retry budget exhausted.
|
|
94
|
+
- slug: invite-code-generator
|
|
95
|
+
kind: pure-fn
|
|
96
|
+
role: Pure helper that turns random bytes into a printable invite code.
|
|
97
|
+
functions:
|
|
98
|
+
- name: encode
|
|
99
|
+
in: bytes[8]
|
|
100
|
+
out: code
|
|
101
|
+
side: pure
|
|
102
|
+
purpose: Base32 encoding without padding for human-readable codes.
|
|
103
|
+
variables: []
|
|
104
|
+
dataflow:
|
|
105
|
+
- Read 8 random bytes.
|
|
106
|
+
- Base32 encode without padding.
|
|
107
|
+
errors: []
|
|
108
|
+
- slug: postgresql
|
|
109
|
+
kind: db
|
|
110
|
+
role: Owns the `invite_codes` table (producer side of the cross-feature data row).
|
|
111
|
+
functions:
|
|
112
|
+
- name: INSERT_invite_codes
|
|
113
|
+
in: row
|
|
114
|
+
out: rowid
|
|
115
|
+
side: write
|
|
116
|
+
purpose: Persists the issued invite row.
|
|
117
|
+
variables:
|
|
118
|
+
- name: invite_codes.code
|
|
119
|
+
type: text
|
|
120
|
+
scope: persist
|
|
121
|
+
purpose: Unique business key consumed by the registration flow.
|
|
122
|
+
- name: invite_codes.consumed_at
|
|
123
|
+
type: timestamptz
|
|
124
|
+
scope: persist
|
|
125
|
+
purpose: Stays null until the registration flow consumes the code.
|
|
126
|
+
dataflow:
|
|
127
|
+
- Accept INSERT from invite-issuance-service.
|
|
128
|
+
- Enforce unique constraint on code.
|
|
129
|
+
- Return new rowid.
|
|
130
|
+
errors:
|
|
131
|
+
- name: ErrUniqueViolation
|
|
132
|
+
when: Two issuance attempts pick the same code.
|
|
133
|
+
means: Bubble up so issuance can retry.
|
|
134
|
+
edges:
|
|
135
|
+
- id: e-ui-api
|
|
136
|
+
from: web-get-invite-ui
|
|
137
|
+
to: public-api
|
|
138
|
+
kind: call
|
|
139
|
+
label: POST /api/invites
|
|
140
|
+
- id: e-api-svc
|
|
141
|
+
from: public-api
|
|
142
|
+
to: invite-issuance-service
|
|
143
|
+
kind: call
|
|
144
|
+
label: Issue(ctx, userId)
|
|
145
|
+
- id: e-svc-gen
|
|
146
|
+
from: invite-issuance-service
|
|
147
|
+
to: invite-code-generator
|
|
148
|
+
kind: call
|
|
149
|
+
label: encode(rand)
|
|
150
|
+
- id: e-svc-db
|
|
151
|
+
from: invite-issuance-service
|
|
152
|
+
to: postgresql
|
|
153
|
+
kind: call
|
|
154
|
+
label: INSERT invite_codes
|
|
155
|
+
- id: e-db-svc-return
|
|
156
|
+
from: postgresql
|
|
157
|
+
to: invite-issuance-service
|
|
158
|
+
kind: return
|
|
159
|
+
label: rowid
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
slug: invite-code-registration
|
|
2
|
+
title: Invite-code registration
|
|
3
|
+
story: >-
|
|
4
|
+
A visitor holding an invite code submits email/password plus the code. The registration
|
|
5
|
+
service validates the code inside a single transaction; on success it creates a new user
|
|
6
|
+
and consumes the invite_codes row issued earlier by `get-invite-codes`.
|
|
7
|
+
dependsOn:
|
|
8
|
+
- get-invite-codes
|
|
9
|
+
submodules:
|
|
10
|
+
- slug: web-register-ui
|
|
11
|
+
kind: ui
|
|
12
|
+
role: React page that captures email, password, and invite code.
|
|
13
|
+
functions:
|
|
14
|
+
- name: handleSubmit
|
|
15
|
+
in: FormEvent
|
|
16
|
+
out: void
|
|
17
|
+
side: io
|
|
18
|
+
purpose: POSTs the registration payload and routes the response.
|
|
19
|
+
variables:
|
|
20
|
+
- name: email
|
|
21
|
+
type: string
|
|
22
|
+
scope: call
|
|
23
|
+
purpose: Identity for the new account; required.
|
|
24
|
+
- name: inviteCode
|
|
25
|
+
type: string
|
|
26
|
+
scope: call
|
|
27
|
+
purpose: Token from the off-platform invite hand-off.
|
|
28
|
+
dataflow:
|
|
29
|
+
- Collect form fields.
|
|
30
|
+
- POST /api/register with payload.
|
|
31
|
+
- On 2xx redirect to /welcome; otherwise surface field-level errors.
|
|
32
|
+
errors:
|
|
33
|
+
- name: ErrInvalidCode
|
|
34
|
+
when: API returns 422 with `invite_code` reason.
|
|
35
|
+
means: Highlight the invite-code field with the API message.
|
|
36
|
+
- slug: public-api
|
|
37
|
+
kind: api
|
|
38
|
+
role: HTTP boundary for `/api/register` POST requests.
|
|
39
|
+
functions:
|
|
40
|
+
- name: postRegister
|
|
41
|
+
in: HttpRequest
|
|
42
|
+
out: HttpResponse
|
|
43
|
+
side: network
|
|
44
|
+
purpose: Parses payload and delegates to registration-service.
|
|
45
|
+
variables:
|
|
46
|
+
- name: payload
|
|
47
|
+
type: RegisterDTO
|
|
48
|
+
scope: call
|
|
49
|
+
purpose: Inbound registration intent.
|
|
50
|
+
dataflow:
|
|
51
|
+
- Decode JSON body.
|
|
52
|
+
- Call registration-service.RegisterWithInvite.
|
|
53
|
+
- Serialize success or 422 / 5xx error.
|
|
54
|
+
errors:
|
|
55
|
+
- name: ErrMalformedPayload
|
|
56
|
+
when: JSON parse fails.
|
|
57
|
+
means: 400 response with `payload` reason.
|
|
58
|
+
- slug: registration-service
|
|
59
|
+
kind: service
|
|
60
|
+
role: Owns the registration transaction (consumer side of the invite_codes data row).
|
|
61
|
+
functions:
|
|
62
|
+
- name: RegisterWithInvite
|
|
63
|
+
in: ctx, RegisterInput
|
|
64
|
+
out: NewUserID | ErrInvalidCode | ErrEmailTaken
|
|
65
|
+
side: tx
|
|
66
|
+
purpose: Validates code, hashes password, inserts user, consumes invite.
|
|
67
|
+
- name: decideInviteValid
|
|
68
|
+
in: row, now
|
|
69
|
+
out: true | ErrInvalidCode
|
|
70
|
+
side: pure
|
|
71
|
+
purpose: Checks existence, expiry, consumption state.
|
|
72
|
+
variables:
|
|
73
|
+
- name: inviteRow
|
|
74
|
+
type: InviteCodeRow
|
|
75
|
+
scope: tx
|
|
76
|
+
purpose: Row read with SELECT FOR UPDATE; gates the rest of the flow.
|
|
77
|
+
- name: passwordHash
|
|
78
|
+
type: string
|
|
79
|
+
scope: tx
|
|
80
|
+
purpose: bcrypt/argon2 hash of the submitted password; plaintext is discarded.
|
|
81
|
+
dataflow:
|
|
82
|
+
- Open transaction.
|
|
83
|
+
- SELECT invite_codes FOR UPDATE WHERE code = ?
|
|
84
|
+
- decideInviteValid against current time.
|
|
85
|
+
- INSERT users row and UPDATE invite_codes.consumed_at.
|
|
86
|
+
- Commit and return NewUserID.
|
|
87
|
+
errors:
|
|
88
|
+
- name: ErrInvalidCode
|
|
89
|
+
when: Row missing, expired, or already consumed.
|
|
90
|
+
means: 422 response with `invite_code` reason.
|
|
91
|
+
- name: ErrEmailTaken
|
|
92
|
+
when: Unique violation on users.email.
|
|
93
|
+
means: 422 response with `email` reason.
|
|
94
|
+
- slug: postgresql
|
|
95
|
+
kind: db
|
|
96
|
+
role: Stores `users` rows and applies invite-code state transitions inside the registration tx.
|
|
97
|
+
functions:
|
|
98
|
+
- name: SELECT_invite_for_update
|
|
99
|
+
in: code
|
|
100
|
+
out: InviteCodeRow | nil
|
|
101
|
+
side: lock
|
|
102
|
+
purpose: Locks the invite row for the duration of the transaction.
|
|
103
|
+
- name: INSERT_users
|
|
104
|
+
in: userRow
|
|
105
|
+
out: userId
|
|
106
|
+
side: write
|
|
107
|
+
purpose: Persists the new account.
|
|
108
|
+
- name: UPDATE_invite_consumed
|
|
109
|
+
in: code, now
|
|
110
|
+
out: rows_affected
|
|
111
|
+
side: write
|
|
112
|
+
purpose: Marks the invite as consumed.
|
|
113
|
+
variables:
|
|
114
|
+
- name: users.email
|
|
115
|
+
type: text
|
|
116
|
+
scope: persist
|
|
117
|
+
purpose: Unique account identity.
|
|
118
|
+
- name: invite_codes.consumed_at
|
|
119
|
+
type: timestamptz
|
|
120
|
+
scope: persist
|
|
121
|
+
purpose: Timestamp set the moment the invite row is consumed.
|
|
122
|
+
dataflow:
|
|
123
|
+
- Apply row lock on invite_codes.
|
|
124
|
+
- Validate uniqueness of users.email.
|
|
125
|
+
- Write users row and update invite_codes.consumed_at.
|
|
126
|
+
errors:
|
|
127
|
+
- name: ErrUniqueEmail
|
|
128
|
+
when: users.email unique constraint violated.
|
|
129
|
+
means: Bubble up so service can return ErrEmailTaken.
|
|
130
|
+
edges:
|
|
131
|
+
- id: e-ui-api
|
|
132
|
+
from: web-register-ui
|
|
133
|
+
to: public-api
|
|
134
|
+
kind: call
|
|
135
|
+
label: POST /api/register
|
|
136
|
+
- id: e-api-svc
|
|
137
|
+
from: public-api
|
|
138
|
+
to: registration-service
|
|
139
|
+
kind: call
|
|
140
|
+
label: RegisterWithInvite(ctx, RegisterInput)
|
|
141
|
+
- id: e-svc-db-select
|
|
142
|
+
from: registration-service
|
|
143
|
+
to: postgresql
|
|
144
|
+
kind: call
|
|
145
|
+
label: SELECT invite_codes FOR UPDATE
|
|
146
|
+
- id: e-svc-db-insert
|
|
147
|
+
from: registration-service
|
|
148
|
+
to: postgresql
|
|
149
|
+
kind: call
|
|
150
|
+
label: INSERT users
|
|
151
|
+
- id: e-svc-db-update
|
|
152
|
+
from: registration-service
|
|
153
|
+
to: postgresql
|
|
154
|
+
kind: call
|
|
155
|
+
label: UPDATE invite_codes.consumed_at
|
|
156
|
+
- id: e-db-svc-return
|
|
157
|
+
from: postgresql
|
|
158
|
+
to: registration-service
|
|
159
|
+
kind: return
|
|
160
|
+
label: row | rows_affected
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-atlas-page="feature">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Get invite codes</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<link rel="stylesheet" href="../../assets/architecture.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header class="feature-header">
|
|
11
|
+
<nav class="feature-breadcrumb"><a href="../../index.html">← Atlas</a></nav>
|
|
12
|
+
<h1>Get invite codes</h1>
|
|
13
|
+
|
|
14
|
+
</header>
|
|
15
|
+
<main class="feature-main">
|
|
16
|
+
<section class="feature-story"><p>An authenticated member requests a new invite code that they can hand to an off-platform visitor. The issuance service mints a fresh code, persists it in `invite_codes`, and returns the code string to the UI. Issued rows then become the producer side of the `invite-code-registration` flow.</p></section>
|
|
17
|
+
<section class="feature-submodules" aria-label="Submodules">
|
|
18
|
+
<h2>Submodules</h2>
|
|
19
|
+
<ul class="submodule-nav">
|
|
20
|
+
<li class="submodule-card">
|
|
21
|
+
<a class="submodule-card__link" href="web-get-invite-ui.html">
|
|
22
|
+
<span class="submodule-card__name">web-get-invite-ui</span>
|
|
23
|
+
<span class="submodule-card__kind submodule-card__kind--ui">UI</span>
|
|
24
|
+
</a>
|
|
25
|
+
<p class="submodule-card__role">React page that lets a signed-in member request a new invite code.</p>
|
|
26
|
+
</li>
|
|
27
|
+
<li class="submodule-card">
|
|
28
|
+
<a class="submodule-card__link" href="public-api.html">
|
|
29
|
+
<span class="submodule-card__name">public-api</span>
|
|
30
|
+
<span class="submodule-card__kind submodule-card__kind--api">API</span>
|
|
31
|
+
</a>
|
|
32
|
+
<p class="submodule-card__role">HTTP boundary for `/api/invites` POST requests.</p>
|
|
33
|
+
</li>
|
|
34
|
+
<li class="submodule-card">
|
|
35
|
+
<a class="submodule-card__link" href="invite-issuance-service.html">
|
|
36
|
+
<span class="submodule-card__name">invite-issuance-service</span>
|
|
37
|
+
<span class="submodule-card__kind submodule-card__kind--service">Service</span>
|
|
38
|
+
</a>
|
|
39
|
+
<p class="submodule-card__role">Domain service that mints and persists a single invite row per request.</p>
|
|
40
|
+
</li>
|
|
41
|
+
<li class="submodule-card">
|
|
42
|
+
<a class="submodule-card__link" href="invite-code-generator.html">
|
|
43
|
+
<span class="submodule-card__name">invite-code-generator</span>
|
|
44
|
+
<span class="submodule-card__kind submodule-card__kind--pure-fn">Pure fn</span>
|
|
45
|
+
</a>
|
|
46
|
+
<p class="submodule-card__role">Pure helper that turns random bytes into a printable invite code.</p>
|
|
47
|
+
</li>
|
|
48
|
+
<li class="submodule-card">
|
|
49
|
+
<a class="submodule-card__link" href="postgresql.html">
|
|
50
|
+
<span class="submodule-card__name">postgresql</span>
|
|
51
|
+
<span class="submodule-card__kind submodule-card__kind--db">DB</span>
|
|
52
|
+
</a>
|
|
53
|
+
<p class="submodule-card__role">Owns the `invite_codes` table (producer side of the cross-feature data row).</p>
|
|
54
|
+
</li>
|
|
55
|
+
</ul>
|
|
56
|
+
</section>
|
|
57
|
+
<section class="feature-edges" aria-label="Intra-feature edges">
|
|
58
|
+
<h2>Intra-feature edges</h2>
|
|
59
|
+
<ul class="feature-edges__list">
|
|
60
|
+
<li class="feature-edges__item feature-edges__item--call"><span class="feature-edges__endpoints">web-get-invite-ui → public-api</span><span class="feature-edges__kind">call</span><span class="feature-edges__label">POST /api/invites</span></li>
|
|
61
|
+
<li class="feature-edges__item feature-edges__item--call"><span class="feature-edges__endpoints">public-api → invite-issuance-service</span><span class="feature-edges__kind">call</span><span class="feature-edges__label">Issue(ctx, userId)</span></li>
|
|
62
|
+
<li class="feature-edges__item feature-edges__item--call"><span class="feature-edges__endpoints">invite-issuance-service → invite-code-generator</span><span class="feature-edges__kind">call</span><span class="feature-edges__label">encode(rand)</span></li>
|
|
63
|
+
<li class="feature-edges__item feature-edges__item--call"><span class="feature-edges__endpoints">invite-issuance-service → postgresql</span><span class="feature-edges__kind">call</span><span class="feature-edges__label">INSERT invite_codes</span></li>
|
|
64
|
+
<li class="feature-edges__item feature-edges__item--return"><span class="feature-edges__endpoints">postgresql → invite-issuance-service</span><span class="feature-edges__kind">return</span><span class="feature-edges__label">rowid</span></li>
|
|
65
|
+
</ul>
|
|
66
|
+
</section>
|
|
67
|
+
</main>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|