@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,67 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-atlas-page="submodule">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Invite-code registration · web-register-ui</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="submodule-header">
11
+ <nav class="submodule-breadcrumb"><a href="../../index.html">← Atlas</a> · <a href="index.html">← Invite-code registration</a></nav>
12
+ <h1>web-register-ui <small class="submodule-kind submodule-kind--ui">UI</small></h1>
13
+ <p class="submodule-role">React page that captures email, password, and invite code.</p>
14
+ </header>
15
+ <main class="submodule-main">
16
+ <section class="sub-io" aria-label="Function I/O">
17
+ <h2>Function I/O</h2>
18
+ <table class="sub-table">
19
+ <thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Out</th><th scope="col">Side</th><th scope="col">Purpose</th></tr></thead>
20
+ <tbody>
21
+ <tr><td>handleSubmit</td><td>FormEvent</td><td>void</td><td>io</td><td>POSTs the registration payload and routes the response.</td></tr>
22
+ </tbody>
23
+ </table>
24
+ </section>
25
+ <section class="sub-vars" aria-label="Variables">
26
+ <h2>Variables</h2>
27
+ <table class="sub-table">
28
+ <thead><tr><th scope="col">Name</th><th scope="col">Type</th><th scope="col">Scope</th><th scope="col">Purpose</th></tr></thead>
29
+ <tbody>
30
+ <tr><td>email</td><td>string</td><td>call</td><td>Identity for the new account; required.</td></tr>
31
+ <tr><td>inviteCode</td><td>string</td><td>call</td><td>Token from the off-platform invite hand-off.</td></tr>
32
+ </tbody>
33
+ </table>
34
+ </section>
35
+ <section class="sub-dataflow" aria-label="Internal data flow">
36
+ <h2>Internal data flow</h2>
37
+ <svg class="sub-dataflow__svg" viewBox="0 0 400 264" role="img" aria-label="Internal dataflow">
38
+ <defs><marker id="sub-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 Z" /></marker></defs>
39
+ <g class="sub-dataflow__step">
40
+ <rect x="20" y="20" width="360" height="56" rx="8" ry="8" />
41
+ <text x="200" y="44" text-anchor="middle">Collect form fields.</text>
42
+ </g>
43
+ <line class="sub-dataflow__arrow" x1="200" y1="76" x2="200" y2="104" marker-end="url(#sub-arrow)" />
44
+ <g class="sub-dataflow__step">
45
+ <rect x="20" y="104" width="360" height="56" rx="8" ry="8" />
46
+ <text x="200" y="128" text-anchor="middle">POST /api/register with payload.</text>
47
+ </g>
48
+ <line class="sub-dataflow__arrow" x1="200" y1="160" x2="200" y2="188" marker-end="url(#sub-arrow)" />
49
+ <g class="sub-dataflow__step">
50
+ <rect x="20" y="188" width="360" height="56" rx="8" ry="8" />
51
+ <text x="200" y="212" text-anchor="middle">On 2xx redirect to /welcome; otherwise surface</text>
52
+ <text x="200" y="228" text-anchor="middle">field-level errors.</text>
53
+ </g>
54
+ </svg>
55
+ </section>
56
+ <section class="sub-errors" aria-label="Errors">
57
+ <h2>Errors</h2>
58
+ <table class="sub-table">
59
+ <thead><tr><th scope="col">Name</th><th scope="col">When</th><th scope="col">Means</th></tr></thead>
60
+ <tbody>
61
+ <tr><td>ErrInvalidCode</td><td>API returns 422 with `invite_code` reason.</td><td>Highlight the invite-code field with the API message.</td></tr>
62
+ </tbody>
63
+ </table>
64
+ </section>
65
+ </main>
66
+ </body>
67
+ </html>
@@ -0,0 +1,234 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-atlas-page="macro">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Acme App — sample atlas</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="atlas-header">
11
+ <h1>Acme App — sample atlas</h1>
12
+ <p class="atlas-summary">Two end-to-end feature modules — minting invite codes and consuming them during account registration — produced by `apltk architecture` from a declarative YAML source. The macro diagram surfaces both feature clusters and every sub-module they own, plus the cross-feature data-row that carries an `invite_codes` row between them.</p>
13
+ </header>
14
+ <main class="atlas-main">
15
+ <section class="atlas-canvas" aria-label="Macro architecture diagram">
16
+ <div class="atlas-canvas__toolbar" role="toolbar" aria-label="Diagram controls">
17
+ <button type="button" data-pan-zoom="zoom-in" aria-label="Zoom in">+</button>
18
+ <button type="button" data-pan-zoom="zoom-out" aria-label="Zoom out">−</button>
19
+ <button type="button" data-pan-zoom="fit" aria-label="Reset view">Fit</button>
20
+ </div>
21
+ <div class="atlas-canvas__viewport" data-pan-zoom-viewport>
22
+ <svg class="atlas-svg" viewBox="0 0 3989 453" role="img" aria-label="Project architecture atlas" data-atlas-svg="macro">
23
+ <defs>
24
+ <marker id="arrow-call" class="m-arrow m-arrow--call" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 Z" /></marker>
25
+ <marker id="arrow-return" class="m-arrow m-arrow--return" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 Z" /></marker>
26
+ <marker id="arrow-data-row" class="m-arrow m-arrow--data-row" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 Z" /></marker>
27
+ <marker id="arrow-failure" class="m-arrow m-arrow--failure" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 Z" /></marker>
28
+ </defs>
29
+ <g transform="translate(24,24)">
30
+ <g class="m-cluster" data-feature="get-invite-codes">
31
+ <rect class="m-cluster__bg" x="40.00" y="40.00" width="1650.00" height="325.00" rx="14" ry="14" />
32
+ <text class="m-cluster__title" x="865.00" y="66.00" text-anchor="middle">Get invite codes</text>
33
+ </g>
34
+ <g class="m-cluster" data-feature="invite-code-registration">
35
+ <rect class="m-cluster__bg" x="2091.00" y="100.00" width="1810.00" height="253.27" rx="14" ry="14" />
36
+ <text class="m-cluster__title" x="2996.00" y="126.00" text-anchor="middle">Invite-code registration</text>
37
+ </g>
38
+ <a class="m-node m-node--ui" href="features/get-invite-codes/web-get-invite-ui.html" data-feature="get-invite-codes" data-submodule="web-get-invite-ui">
39
+ <rect x="64.00" y="226.80" width="240.00" height="92.00" rx="10" ry="10" />
40
+ <text class="m-node__title" x="184.00" y="254.80" text-anchor="middle">web-get-invite-ui</text>
41
+ <text class="m-node__kind" x="184.00" y="278.80" text-anchor="middle">UI</text>
42
+ <text class="m-node__role" x="184.00" y="304.80" text-anchor="middle">React page that lets a signed-in mem…</text>
43
+ </a>
44
+ <a class="m-node m-node--api" href="features/get-invite-codes/public-api.html" data-feature="get-invite-codes" data-submodule="public-api">
45
+ <rect x="511.00" y="226.80" width="240.00" height="92.00" rx="10" ry="10" />
46
+ <text class="m-node__title" x="631.00" y="254.80" text-anchor="middle">public-api</text>
47
+ <text class="m-node__kind" x="631.00" y="278.80" text-anchor="middle">API</text>
48
+ <text class="m-node__role" x="631.00" y="304.80" text-anchor="middle">HTTP boundary for `/api/invites` POS…</text>
49
+ </a>
50
+ <a class="m-node m-node--service" href="features/get-invite-codes/invite-issuance-service.html" data-feature="get-invite-codes" data-submodule="invite-issuance-service">
51
+ <rect x="965.00" y="226.80" width="240.00" height="92.00" rx="10" ry="10" />
52
+ <text class="m-node__title" x="1085.00" y="254.80" text-anchor="middle">invite-issuance-service</text>
53
+ <text class="m-node__kind" x="1085.00" y="278.80" text-anchor="middle">Service</text>
54
+ <text class="m-node__role" x="1085.00" y="304.80" text-anchor="middle">Domain service that mints and persis…</text>
55
+ </a>
56
+ <a class="m-node m-node--pure-fn" href="features/get-invite-codes/invite-code-generator.html" data-feature="get-invite-codes" data-submodule="invite-code-generator">
57
+ <rect x="1426.00" y="129.00" width="240.00" height="92.00" rx="10" ry="10" />
58
+ <text class="m-node__title" x="1546.00" y="157.00" text-anchor="middle">invite-code-generator</text>
59
+ <text class="m-node__kind" x="1546.00" y="181.00" text-anchor="middle">Pure fn</text>
60
+ <text class="m-node__role" x="1546.00" y="207.00" text-anchor="middle">Pure helper that turns random bytes …</text>
61
+ </a>
62
+ <a class="m-node m-node--db" href="features/get-invite-codes/postgresql.html" data-feature="get-invite-codes" data-submodule="postgresql">
63
+ <rect x="1426.00" y="245.00" width="240.00" height="92.00" rx="10" ry="10" />
64
+ <text class="m-node__title" x="1546.00" y="273.00" text-anchor="middle">postgresql</text>
65
+ <text class="m-node__kind" x="1546.00" y="297.00" text-anchor="middle">DB</text>
66
+ <text class="m-node__role" x="1546.00" y="323.00" text-anchor="middle">Owns the `invite_codes` table (produ…</text>
67
+ </a>
68
+ <a class="m-node m-node--ui" href="features/invite-code-registration/web-register-ui.html" data-feature="invite-code-registration" data-submodule="web-register-ui">
69
+ <rect x="2115.00" y="189.00" width="240.00" height="92.00" rx="10" ry="10" />
70
+ <text class="m-node__title" x="2235.00" y="217.00" text-anchor="middle">web-register-ui</text>
71
+ <text class="m-node__kind" x="2235.00" y="241.00" text-anchor="middle">UI</text>
72
+ <text class="m-node__role" x="2235.00" y="267.00" text-anchor="middle">React page that captures email, pass…</text>
73
+ </a>
74
+ <a class="m-node m-node--api" href="features/invite-code-registration/public-api.html" data-feature="invite-code-registration" data-submodule="public-api">
75
+ <rect x="2569.00" y="189.00" width="240.00" height="92.00" rx="10" ry="10" />
76
+ <text class="m-node__title" x="2689.00" y="217.00" text-anchor="middle">public-api</text>
77
+ <text class="m-node__kind" x="2689.00" y="241.00" text-anchor="middle">API</text>
78
+ <text class="m-node__role" x="2689.00" y="267.00" text-anchor="middle">HTTP boundary for `/api/register` PO…</text>
79
+ </a>
80
+ <a class="m-node m-node--service" href="features/invite-code-registration/registration-service.html" data-feature="invite-code-registration" data-submodule="registration-service">
81
+ <rect x="3101.00" y="229.67" width="240.00" height="92.00" rx="10" ry="10" />
82
+ <text class="m-node__title" x="3221.00" y="257.67" text-anchor="middle">registration-service</text>
83
+ <text class="m-node__kind" x="3221.00" y="281.67" text-anchor="middle">Service</text>
84
+ <text class="m-node__role" x="3221.00" y="307.67" text-anchor="middle">Owns the registration transaction (c…</text>
85
+ </a>
86
+ <a class="m-node m-node--db" href="features/invite-code-registration/postgresql.html" data-feature="invite-code-registration" data-submodule="postgresql">
87
+ <rect x="3637.00" y="216.07" width="240.00" height="92.00" rx="10" ry="10" />
88
+ <text class="m-node__title" x="3757.00" y="244.07" text-anchor="middle">postgresql</text>
89
+ <text class="m-node__kind" x="3757.00" y="268.07" text-anchor="middle">DB</text>
90
+ <text class="m-node__role" x="3757.00" y="294.07" text-anchor="middle">Stores `users` rows and applies invi…</text>
91
+ </a>
92
+ <g class="m-edge m-edge--data-row" data-edge="cross-issuance-to-postgres-codes">
93
+ <path d="M1165.00,260.40 L1175.00,260.40 L1175.00,274.00 L1386.00,274.00" fill="none" marker-end="url(#arrow-data-row)" />
94
+ <text class="m-edge__label" x="1275.50" y="290.00" text-anchor="middle">INSERT invite_codes</text>
95
+ </g>
96
+ <g class="m-edge m-edge--data-row" data-edge="cross-postgres-codes-to-registration">
97
+ <path d="M1666.00,291.00 L3101.00,291.00" fill="none" marker-end="url(#arrow-data-row)" />
98
+ <text class="m-edge__label" x="1890.50" y="307.00" text-anchor="middle">read/consume invite_codes</text>
99
+ </g>
100
+ <g class="m-edge m-edge--call" data-edge="e-ui-api">
101
+ <path d="M304.00,272.80 L511.00,272.80" fill="none" marker-end="url(#arrow-call)" />
102
+ <text class="m-edge__label" x="407.50" y="288.80" text-anchor="middle">POST /api/invites</text>
103
+ </g>
104
+ <g class="m-edge m-edge--call" data-edge="e-api-svc">
105
+ <path d="M751.00,272.80 L965.00,272.80" fill="none" marker-end="url(#arrow-call)" />
106
+ <text class="m-edge__label" x="858.00" y="288.80" text-anchor="middle">Issue(ctx, userId)</text>
107
+ </g>
108
+ <g class="m-edge m-edge--call" data-edge="e-svc-gen">
109
+ <path d="M1205.00,245.20 L1215.00,245.20 L1215.00,175.00 L1426.00,175.00" fill="none" marker-end="url(#arrow-call)" />
110
+ <text class="m-edge__label" x="1315.50" y="191.00" text-anchor="middle">encode(rand)</text>
111
+ </g>
112
+ <g class="m-edge m-edge--call" data-edge="e-svc-db">
113
+ <path d="M1205.00,282.00 L1400.00,282.00 L1400.00,291.00 L1426.00,291.00" fill="none" marker-end="url(#arrow-call)" />
114
+ <text class="m-edge__label" x="1315.50" y="298.00" text-anchor="middle">INSERT invite_codes</text>
115
+ </g>
116
+ <g class="m-edge m-edge--return" data-edge="e-db-svc-return">
117
+ <path d="M1426.00,268.00 L1400.00,268.00 L1400.00,250.00 L1225.00,250.00 L1225.00,263.60 L1205.00,263.60" fill="none" marker-end="url(#arrow-return)" />
118
+ <text class="m-edge__label" x="1315.50" y="266.00" text-anchor="middle">rowid</text>
119
+ </g>
120
+ <g class="m-edge m-edge--call" data-edge="e-ui-api">
121
+ <path d="M2355.00,235.00 L2569.00,235.00" fill="none" marker-end="url(#arrow-call)" />
122
+ <text class="m-edge__label" x="2462.00" y="227.00" text-anchor="middle">POST /api/register</text>
123
+ </g>
124
+ <g class="m-edge m-edge--call" data-edge="e-api-svc">
125
+ <path d="M2809.00,235.00 L3075.00,235.00 L3075.00,260.33 L3101.00,260.33" fill="none" marker-end="url(#arrow-call)" />
126
+ <text class="m-edge__label" x="2955.00" y="227.00" text-anchor="middle">RegisterWithInvite(ctx, RegisterInput)</text>
127
+ </g>
128
+ <g class="m-edge m-edge--call" data-edge="e-svc-db-select">
129
+ <path d="M3341.00,266.47 L3361.00,266.47 L3361.00,239.27 L3611.00,239.27 L3611.00,252.87 L3637.00,252.87" fill="none" marker-end="url(#arrow-call)" />
130
+ <text class="m-edge__label" x="3491.00" y="255.27" text-anchor="middle">SELECT invite_codes FOR UPDATE</text>
131
+ </g>
132
+ <g class="m-edge m-edge--call" data-edge="e-svc-db-insert">
133
+ <path d="M3341.00,284.87 L3371.00,284.87 L3371.00,271.27 L3637.00,271.27" fill="none" marker-end="url(#arrow-call)" />
134
+ <text class="m-edge__label" x="3491.00" y="287.27" text-anchor="middle">INSERT users</text>
135
+ </g>
136
+ <g class="m-edge m-edge--call" data-edge="e-svc-db-update">
137
+ <path d="M3341.00,303.27 L3611.00,303.27 L3611.00,289.67 L3637.00,289.67" fill="none" marker-end="url(#arrow-call)" />
138
+ <text class="m-edge__label" x="3491.00" y="319.27" text-anchor="middle">UPDATE invite_codes.consumed_at</text>
139
+ </g>
140
+ <g class="m-edge m-edge--return" data-edge="e-db-svc-return">
141
+ <path d="M3637.00,234.47 L3621.00,234.47 L3621.00,207.27 L3351.00,207.27 L3351.00,248.07 L3341.00,248.07" fill="none" marker-end="url(#arrow-return)" />
142
+ <text class="m-edge__label" x="3491.00" y="223.27" text-anchor="middle">row | rows_affected</text>
143
+ </g>
144
+ </g>
145
+ </svg>
146
+ </div>
147
+ <ol class="atlas-legend" aria-label="Edge legend">
148
+ <li><span class="legend-swatch legend-swatch--call"></span>call</li>
149
+ <li><span class="legend-swatch legend-swatch--return"></span>return</li>
150
+ <li><span class="legend-swatch legend-swatch--data-row"></span>data-row</li>
151
+ <li><span class="legend-swatch legend-swatch--failure"></span>failure</li>
152
+ </ol>
153
+ </section>
154
+ <section class="atlas-index" aria-label="Submodule index">
155
+ <h2>Submodule index</h2>
156
+ <ul class="atlas-submodule-index">
157
+ <li class="atlas-submodule-index__item">
158
+ <a href="features/get-invite-codes/web-get-invite-ui.html">
159
+ <span class="atlas-submodule-index__feature">Get invite codes</span>
160
+ <span class="atlas-submodule-index__sub">web-get-invite-ui</span>
161
+ <span class="atlas-submodule-index__kind atlas-submodule-index__kind--ui">UI</span>
162
+ </a>
163
+ <p class="atlas-submodule-index__role">React page that lets a signed-in member request a new invite code.</p>
164
+ </li>
165
+ <li class="atlas-submodule-index__item">
166
+ <a href="features/get-invite-codes/public-api.html">
167
+ <span class="atlas-submodule-index__feature">Get invite codes</span>
168
+ <span class="atlas-submodule-index__sub">public-api</span>
169
+ <span class="atlas-submodule-index__kind atlas-submodule-index__kind--api">API</span>
170
+ </a>
171
+ <p class="atlas-submodule-index__role">HTTP boundary for `/api/invites` POST requests.</p>
172
+ </li>
173
+ <li class="atlas-submodule-index__item">
174
+ <a href="features/get-invite-codes/invite-issuance-service.html">
175
+ <span class="atlas-submodule-index__feature">Get invite codes</span>
176
+ <span class="atlas-submodule-index__sub">invite-issuance-service</span>
177
+ <span class="atlas-submodule-index__kind atlas-submodule-index__kind--service">Service</span>
178
+ </a>
179
+ <p class="atlas-submodule-index__role">Domain service that mints and persists a single invite row per request.</p>
180
+ </li>
181
+ <li class="atlas-submodule-index__item">
182
+ <a href="features/get-invite-codes/invite-code-generator.html">
183
+ <span class="atlas-submodule-index__feature">Get invite codes</span>
184
+ <span class="atlas-submodule-index__sub">invite-code-generator</span>
185
+ <span class="atlas-submodule-index__kind atlas-submodule-index__kind--pure-fn">Pure fn</span>
186
+ </a>
187
+ <p class="atlas-submodule-index__role">Pure helper that turns random bytes into a printable invite code.</p>
188
+ </li>
189
+ <li class="atlas-submodule-index__item">
190
+ <a href="features/get-invite-codes/postgresql.html">
191
+ <span class="atlas-submodule-index__feature">Get invite codes</span>
192
+ <span class="atlas-submodule-index__sub">postgresql</span>
193
+ <span class="atlas-submodule-index__kind atlas-submodule-index__kind--db">DB</span>
194
+ </a>
195
+ <p class="atlas-submodule-index__role">Owns the `invite_codes` table (producer side of the cross-feature data row).</p>
196
+ </li>
197
+ <li class="atlas-submodule-index__item">
198
+ <a href="features/invite-code-registration/web-register-ui.html">
199
+ <span class="atlas-submodule-index__feature">Invite-code registration</span>
200
+ <span class="atlas-submodule-index__sub">web-register-ui</span>
201
+ <span class="atlas-submodule-index__kind atlas-submodule-index__kind--ui">UI</span>
202
+ </a>
203
+ <p class="atlas-submodule-index__role">React page that captures email, password, and invite code.</p>
204
+ </li>
205
+ <li class="atlas-submodule-index__item">
206
+ <a href="features/invite-code-registration/public-api.html">
207
+ <span class="atlas-submodule-index__feature">Invite-code registration</span>
208
+ <span class="atlas-submodule-index__sub">public-api</span>
209
+ <span class="atlas-submodule-index__kind atlas-submodule-index__kind--api">API</span>
210
+ </a>
211
+ <p class="atlas-submodule-index__role">HTTP boundary for `/api/register` POST requests.</p>
212
+ </li>
213
+ <li class="atlas-submodule-index__item">
214
+ <a href="features/invite-code-registration/registration-service.html">
215
+ <span class="atlas-submodule-index__feature">Invite-code registration</span>
216
+ <span class="atlas-submodule-index__sub">registration-service</span>
217
+ <span class="atlas-submodule-index__kind atlas-submodule-index__kind--service">Service</span>
218
+ </a>
219
+ <p class="atlas-submodule-index__role">Owns the registration transaction (consumer side of the invite_codes data row).</p>
220
+ </li>
221
+ <li class="atlas-submodule-index__item">
222
+ <a href="features/invite-code-registration/postgresql.html">
223
+ <span class="atlas-submodule-index__feature">Invite-code registration</span>
224
+ <span class="atlas-submodule-index__sub">postgresql</span>
225
+ <span class="atlas-submodule-index__kind atlas-submodule-index__kind--db">DB</span>
226
+ </a>
227
+ <p class="atlas-submodule-index__role">Stores `users` rows and applies invite-code state transitions inside the registration tx.</p>
228
+ </li>
229
+ </ul>
230
+ </section>
231
+ </main>
232
+ <script src="assets/viewer.client.js" defer></script>
233
+ </body>
234
+ </html>
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+ // architecture.js — thin shim over lib/atlas/cli.js.
3
+ //
4
+ // Backward-compatible legacy entrypoint:
5
+ // architecture.js # same as `open`
6
+ // architecture.js open [--project <root>] [--no-open]
7
+ // architecture.js diff [--project <root>] [--out <dir>] [--no-open]
8
+ //
9
+ // All new declarative verbs (feature add, submodule add, function add,
10
+ // variable add, dataflow add|remove|reorder, error add, edge add, meta
11
+ // set, actor add, render, validate, undo) are routed through
12
+ // lib/atlas/cli.js, which owns layout, no-overlap, DOM, CSS, and pan/zoom.
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+ const { spawn } = require('node:child_process');
19
+
20
+ const newCli = require('../lib/atlas/cli');
21
+
22
+ const ATLAS_REL = path.join('resources', 'project-architecture', 'index.html');
23
+ const RESOURCES_REL = path.join('resources', 'project-architecture');
24
+ const PLANS_REL = path.join('docs', 'plans');
25
+ const DIFF_DIRNAME = 'architecture_diff';
26
+ const REMOVED_FILE = '_removed.txt';
27
+ const ATLAS_DIRNAME = 'atlas';
28
+ const DEFAULT_OUT_REL = path.join('.apollo-toolkit', 'architecture-diff');
29
+
30
+ const LEGACY_VERBS = new Set(['open', 'diff']);
31
+
32
+ const USAGE = `apltk architecture — declarative atlas CLI.
33
+
34
+ Usage:
35
+ apltk architecture Open resources/project-architecture/index.html
36
+ apltk architecture open Same as above
37
+ apltk architecture diff Render every architecture_diff/ as one paginated viewer
38
+ apltk architecture render Regenerate atlas HTML from current YAML state
39
+ apltk architecture validate Run schema + referential checks
40
+ apltk architecture feature add|set|remove Manage feature modules
41
+ apltk architecture submodule add|set|remove Manage sub-modules
42
+ apltk architecture function|variable|dataflow|error|edge add|remove
43
+ Manage component rows and edges
44
+ apltk architecture meta set Update meta.title / meta.summary
45
+ apltk architecture actor add|remove Manage top-level actors
46
+ apltk architecture undo Revert the most recent mutation
47
+ apltk architecture --help Show this help
48
+
49
+ Global flags:
50
+ --project <root> Project root (default: nearest ancestor with resources/project-architecture/)
51
+ --spec <spec_dir> Mutations write to <spec_dir>/architecture_diff/atlas/
52
+ --no-render Skip auto-render after a mutation
53
+ --no-open For open/diff: skip launching the browser
54
+ --out <dir> For diff: override viewer output directory
55
+ -h, --help Show this help`;
56
+
57
+ function parseArgs(argv) {
58
+ const args = [...argv];
59
+ const result = {
60
+ subcommand: 'open',
61
+ projectRoot: null,
62
+ out: null,
63
+ open: true,
64
+ help: false,
65
+ };
66
+
67
+ if (args.length > 0 && !args[0].startsWith('-')) {
68
+ const candidate = args[0];
69
+ if (candidate === 'open' || candidate === 'diff') {
70
+ result.subcommand = candidate;
71
+ args.shift();
72
+ }
73
+ }
74
+
75
+ while (args.length > 0) {
76
+ const arg = args.shift();
77
+ if (arg === '--help' || arg === '-h') {
78
+ result.help = true;
79
+ } else if (arg === '--project') {
80
+ const value = args.shift();
81
+ if (!value) throw new Error('Missing value for --project');
82
+ result.projectRoot = path.resolve(value);
83
+ } else if (arg === '--out') {
84
+ const value = args.shift();
85
+ if (!value) throw new Error('Missing value for --out');
86
+ result.out = path.resolve(value);
87
+ } else if (arg === '--no-open') {
88
+ result.open = false;
89
+ } else {
90
+ throw new Error(`Unexpected argument: ${arg}`);
91
+ }
92
+ }
93
+
94
+ return result;
95
+ }
96
+
97
+ function findProjectRoot(startDir) {
98
+ let dir = path.resolve(startDir);
99
+ while (true) {
100
+ if (fs.existsSync(path.join(dir, ATLAS_REL))) return dir;
101
+ if (fs.existsSync(path.join(dir, RESOURCES_REL, ATLAS_DIRNAME, 'atlas.index.yaml'))) return dir;
102
+ const parent = path.dirname(dir);
103
+ if (parent === dir) return null;
104
+ dir = parent;
105
+ }
106
+ }
107
+
108
+ function openInBrowser(filePath) {
109
+ const platform = process.platform;
110
+ let command;
111
+ let args;
112
+ if (platform === 'darwin') { command = 'open'; args = [filePath]; }
113
+ else if (platform === 'win32') { command = 'cmd'; args = ['/c', 'start', '""', filePath]; }
114
+ else { command = 'xdg-open'; args = [filePath]; }
115
+ try {
116
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
117
+ child.on('error', () => {});
118
+ child.unref();
119
+ } catch (_e) { /* best effort */ }
120
+ }
121
+
122
+ function walkArchitectureDiffDirs(plansRoot) {
123
+ const result = [];
124
+ if (!fs.existsSync(plansRoot)) return result;
125
+ function recurse(dir) {
126
+ let entries;
127
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_e) { return; }
128
+ for (const entry of entries) {
129
+ if (!entry.isDirectory()) continue;
130
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
131
+ const full = path.join(dir, entry.name);
132
+ if (entry.name === DIFF_DIRNAME) { result.push(full); continue; }
133
+ recurse(full);
134
+ }
135
+ }
136
+ recurse(plansRoot);
137
+ return result;
138
+ }
139
+
140
+ function walkAfterStateHtml(diffDir) {
141
+ const out = [];
142
+ function recurse(dir, relParts) {
143
+ let entries;
144
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_e) { return; }
145
+ for (const entry of entries) {
146
+ if (entry.name === 'assets') continue;
147
+ if (entry.name === ATLAS_DIRNAME) continue;
148
+ if (entry.name === REMOVED_FILE) continue;
149
+ if (entry.name.startsWith('.')) continue;
150
+ const full = path.join(dir, entry.name);
151
+ const nextRel = [...relParts, entry.name];
152
+ if (entry.isDirectory()) recurse(full, nextRel);
153
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
154
+ out.push({ abs: full, rel: nextRel.join('/') });
155
+ }
156
+ }
157
+ }
158
+ recurse(diffDir, []);
159
+ return out;
160
+ }
161
+
162
+ function readRemovedManifest(diffDir) {
163
+ const manifestPath = path.join(diffDir, REMOVED_FILE);
164
+ if (!fs.existsSync(manifestPath)) return [];
165
+ return fs.readFileSync(manifestPath, 'utf8')
166
+ .split(/\r?\n/)
167
+ .map((line) => line.trim())
168
+ .filter((line) => line && !line.startsWith('#'));
169
+ }
170
+
171
+ function collectChanges(projectRoot) {
172
+ const resourcesRoot = path.join(projectRoot, RESOURCES_REL);
173
+ const plansRoot = path.join(projectRoot, PLANS_REL);
174
+ const diffDirs = walkArchitectureDiffDirs(plansRoot);
175
+ const changes = [];
176
+
177
+ for (const diffDir of diffDirs) {
178
+ const specDir = path.dirname(diffDir);
179
+ const specLabel = path.relative(projectRoot, specDir);
180
+ for (const after of walkAfterStateHtml(diffDir)) {
181
+ const beforeAbs = path.join(resourcesRoot, after.rel);
182
+ const beforeExists = fs.existsSync(beforeAbs);
183
+ changes.push({
184
+ kind: beforeExists ? 'modified' : 'added',
185
+ rel: after.rel,
186
+ spec: specLabel,
187
+ beforePath: beforeExists ? path.relative(projectRoot, beforeAbs) : null,
188
+ afterPath: path.relative(projectRoot, after.abs),
189
+ });
190
+ }
191
+ for (const removedRel of readRemovedManifest(diffDir)) {
192
+ const beforeAbs = path.join(resourcesRoot, removedRel);
193
+ if (!fs.existsSync(beforeAbs)) continue;
194
+ changes.push({
195
+ kind: 'removed',
196
+ rel: removedRel,
197
+ spec: specLabel,
198
+ beforePath: path.relative(projectRoot, beforeAbs),
199
+ afterPath: null,
200
+ });
201
+ }
202
+ }
203
+
204
+ changes.sort((a, b) => {
205
+ if (a.spec !== b.spec) return a.spec.localeCompare(b.spec);
206
+ if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
207
+ return a.rel.localeCompare(b.rel);
208
+ });
209
+ return changes;
210
+ }
211
+
212
+ function toViewerRel(outDir, projectRoot, projectRelPath) {
213
+ if (!projectRelPath) return null;
214
+ const absolute = path.resolve(projectRoot, projectRelPath);
215
+ const rel = path.relative(outDir, absolute);
216
+ return rel.split(path.sep).join('/');
217
+ }
218
+
219
+ function renderViewer({ changes, projectRoot, outDir }) {
220
+ return newCli.renderDiffViewer({ changes, projectRoot, outDir });
221
+ }
222
+
223
+ function runOpen(opts, io) {
224
+ const projectRoot = opts.projectRoot || findProjectRoot(process.cwd());
225
+ if (!projectRoot) {
226
+ io.stderr.write(
227
+ `Could not find resources/project-architecture/index.html. Pass --project <root> or generate the atlas via the init-project-html skill.\n`,
228
+ );
229
+ return 1;
230
+ }
231
+ const atlas = path.join(projectRoot, ATLAS_REL);
232
+ if (!fs.existsSync(atlas)) {
233
+ io.stderr.write(`Atlas not found: ${atlas}\n`);
234
+ return 1;
235
+ }
236
+ io.stdout.write(`${atlas}\n`);
237
+ if (opts.open) openInBrowser(atlas);
238
+ return 0;
239
+ }
240
+
241
+ function runDiff(opts, io) {
242
+ const projectRoot = opts.projectRoot || findProjectRoot(process.cwd());
243
+ if (!projectRoot) {
244
+ io.stderr.write(
245
+ `Could not find resources/project-architecture/index.html. Pass --project <root> or generate the atlas via the init-project-html skill.\n`,
246
+ );
247
+ return 1;
248
+ }
249
+ const outDir = opts.out || path.join(projectRoot, DEFAULT_OUT_REL);
250
+ fs.mkdirSync(outDir, { recursive: true });
251
+
252
+ const changes = collectChanges(projectRoot);
253
+ const html = renderViewer({ changes, projectRoot, outDir });
254
+ const indexPath = path.join(outDir, 'index.html');
255
+ fs.writeFileSync(indexPath, html, 'utf8');
256
+
257
+ io.stdout.write(`${indexPath}\n`);
258
+ io.stdout.write(
259
+ `Diff pages: ${changes.length} (modified=${changes.filter((c) => c.kind === 'modified').length}, added=${changes.filter((c) => c.kind === 'added').length}, removed=${changes.filter((c) => c.kind === 'removed').length})\n`,
260
+ );
261
+ if (opts.open) openInBrowser(indexPath);
262
+ return 0;
263
+ }
264
+
265
+ // main(argv, io) is sync and supports the legacy verbs `open` and
266
+ // `diff` only. Tests rely on the sync return-code contract. All other
267
+ // verbs go through dispatchAsync().
268
+ function main(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
269
+ let opts;
270
+ try {
271
+ opts = parseArgs(argv);
272
+ } catch (error) {
273
+ io.stderr.write(`${error.message}\n\n${USAGE}\n`);
274
+ return 1;
275
+ }
276
+ if (opts.help) {
277
+ io.stdout.write(`${USAGE}\n`);
278
+ return 0;
279
+ }
280
+ if (opts.subcommand === 'open') return runOpen(opts, io);
281
+ if (opts.subcommand === 'diff') return runDiff(opts, io);
282
+ io.stderr.write(`Unknown subcommand: ${opts.subcommand}\n\n${USAGE}\n`);
283
+ return 1;
284
+ }
285
+
286
+ async function dispatchAsync(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
287
+ return newCli.dispatch(argv, io);
288
+ }
289
+
290
+ if (require.main === module) {
291
+ const argv = process.argv.slice(2);
292
+ const verb = argv[0];
293
+ if (!verb || verb.startsWith('-') || LEGACY_VERBS.has(verb)) {
294
+ process.exit(main(argv));
295
+ } else {
296
+ dispatchAsync(argv).then((code) => process.exit(code)).catch((err) => {
297
+ process.stderr.write(`${err && err.stack ? err.stack : err}\n`);
298
+ process.exit(1);
299
+ });
300
+ }
301
+ }
302
+
303
+ module.exports = {
304
+ parseArgs,
305
+ findProjectRoot,
306
+ collectChanges,
307
+ renderViewer,
308
+ toViewerRel,
309
+ walkArchitectureDiffDirs,
310
+ walkAfterStateHtml,
311
+ readRemovedManifest,
312
+ main,
313
+ dispatchAsync,
314
+ };
package/lib/cli.js CHANGED
@@ -158,6 +158,8 @@ function buildHelpText({ version, colorEnabled }) {
158
158
  ' apltk all',
159
159
  ' apltk filter-logs app.log --start 2026-03-24T10:00:00Z',
160
160
  ' apltk create-specs "Membership upgrade flow" --change-name membership-upgrade-flow',
161
+ ' apltk architecture # open resources/project-architecture/index.html',
162
+ ' apltk architecture diff # paginated before/after view of all docs/plans/.../architecture_diff/',
161
163
  ' apltk tools',
162
164
  ' apollo-toolkit all',
163
165
  '',
@@ -3,6 +3,13 @@ const path = require('node:path');
3
3
  const { spawn } = require('node:child_process');
4
4
 
5
5
  const TOOL_COMMANDS = [
6
+ {
7
+ name: 'architecture',
8
+ skill: 'init-project-html',
9
+ script: 'init-project-html/scripts/architecture.js',
10
+ runner: 'node',
11
+ description: 'Open the project HTML architecture atlas, or render a paginated diff (`architecture diff`).',
12
+ },
6
13
  {
7
14
  name: 'filter-logs',
8
15
  skill: 'analyse-app-logs',