@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.
Files changed (55) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +6 -0
  4. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  5. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  6. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  7. package/cjk-pdf/agents/openai.yaml +5 -0
  8. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  9. package/generate-spec/SKILL.md +26 -4
  10. package/generate-spec/agents/openai.yaml +1 -0
  11. package/generate-spec/references/TEMPLATE_SPEC.md +117 -0
  12. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  13. package/init-project-html/SKILL.md +137 -0
  14. package/init-project-html/agents/openai.yaml +22 -0
  15. package/init-project-html/lib/atlas/assets/architecture.css +140 -0
  16. package/init-project-html/lib/atlas/assets/viewer.client.js +93 -0
  17. package/init-project-html/lib/atlas/cli.js +995 -0
  18. package/init-project-html/lib/atlas/layout.js +229 -0
  19. package/init-project-html/lib/atlas/render.js +485 -0
  20. package/init-project-html/lib/atlas/schema.js +310 -0
  21. package/init-project-html/lib/atlas/state.js +402 -0
  22. package/init-project-html/references/TEMPLATE_SPEC.md +137 -0
  23. package/init-project-html/references/architecture-page.template.html +35 -0
  24. package/init-project-html/references/architecture.css +1059 -0
  25. package/init-project-html/sample-demo/resources/project-architecture/assets/architecture.css +140 -0
  26. package/init-project-html/sample-demo/resources/project-architecture/assets/viewer.client.js +93 -0
  27. package/init-project-html/sample-demo/resources/project-architecture/atlas/atlas.index.yaml +34 -0
  28. package/init-project-html/sample-demo/resources/project-architecture/atlas/features/get-invite-codes.yaml +159 -0
  29. package/init-project-html/sample-demo/resources/project-architecture/atlas/features/invite-code-registration.yaml +160 -0
  30. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/index.html +69 -0
  31. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-code-generator.html +50 -0
  32. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-issuance-service.html +72 -0
  33. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/postgresql.html +66 -0
  34. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/public-api.html +70 -0
  35. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/web-get-invite-ui.html +67 -0
  36. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/index.html +63 -0
  37. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/postgresql.html +68 -0
  38. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/public-api.html +65 -0
  39. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/registration-service.html +79 -0
  40. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/web-register-ui.html +67 -0
  41. package/init-project-html/sample-demo/resources/project-architecture/index.html +234 -0
  42. package/init-project-html/scripts/architecture.js +314 -0
  43. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  44. package/lib/cli.js +2 -0
  45. package/lib/tool-runner.js +7 -0
  46. package/merge-conflict-resolver/agents/openai.yaml +5 -0
  47. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  48. package/package.json +6 -2
  49. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  50. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  51. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  52. package/spec-to-project-html/SKILL.md +114 -0
  53. package/spec-to-project-html/agents/openai.yaml +18 -0
  54. package/spec-to-project-html/references/TEMPLATE_SPEC.md +111 -0
  55. 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>