@tecture/shared 0.1.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +218 -0
- package/dist/index.js +174 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @tecture/shared
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- First public release. Exposes the Tecture API/data types (`ApiArchitectureSummary`,
|
|
6
|
+
`ApiDiagram`, `ApiNodeDetail`, `DiagramLayoutFile`, `ManifestFile`, …) plus the runtime
|
|
7
|
+
helpers (`buildArchitectureSummary`, `findNode`, `emptyLayout`, `normalizeLayoutUpdate`,
|
|
8
|
+
`buildSourceUrl`) and the `ArchitectureDataSource` / `LayoutStore` interfaces, so external
|
|
9
|
+
hosts can implement the Tecture data contract over any transport.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shanika Wijerathna
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
declare const SLUG_RE: RegExp;
|
|
2
|
+
declare class DiagramNotFoundError extends Error {
|
|
3
|
+
readonly slug: string;
|
|
4
|
+
constructor(slug: string);
|
|
5
|
+
}
|
|
6
|
+
declare class NodeNotFoundError extends Error {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
constructor(id: string);
|
|
9
|
+
}
|
|
10
|
+
declare class DescriptionNotFoundError extends Error {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
constructor(id: string);
|
|
13
|
+
}
|
|
14
|
+
declare class LayoutInvalidError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
interface ArchitectureDataSource {
|
|
18
|
+
loadManifest(): Promise<ManifestFile>;
|
|
19
|
+
loadDiagram(slug: string): Promise<DiagramFile>;
|
|
20
|
+
loadDescription(nodeId: string): Promise<string>;
|
|
21
|
+
}
|
|
22
|
+
interface LayoutStore {
|
|
23
|
+
loadLayout(slug: string): Promise<DiagramLayoutFile>;
|
|
24
|
+
saveLayout(slug: string, update: ApiDiagramLayoutUpdate): Promise<DiagramLayoutFile>;
|
|
25
|
+
}
|
|
26
|
+
declare function isFiniteNumber(v: unknown): v is number;
|
|
27
|
+
declare function isValidLayoutEntry(v: unknown): v is NodeLayoutEntry;
|
|
28
|
+
declare function emptyLayout(slug: string): DiagramLayoutFile;
|
|
29
|
+
declare function normalizeLayoutUpdate(slug: string, update: ApiDiagramLayoutUpdate): DiagramLayoutFile;
|
|
30
|
+
declare function buildArchitectureSummary(source: ArchitectureDataSource): Promise<ApiArchitectureSummary>;
|
|
31
|
+
/**
|
|
32
|
+
* Build a web URL that views `path` (a repo-root-relative file or directory, trailing "/" = dir)
|
|
33
|
+
* on the source repo host. `HEAD` resolves to the default branch so links stay current. Falls back
|
|
34
|
+
* to the repo root when the host is unknown, and returns undefined when there is no source.
|
|
35
|
+
*/
|
|
36
|
+
declare function buildSourceUrl(source: string | undefined, host: SourceHost | undefined, path: string): string | undefined;
|
|
37
|
+
declare function findNode(source: ArchitectureDataSource, nodeId: string): Promise<{
|
|
38
|
+
node: ArchitectureNode;
|
|
39
|
+
diagramId: string;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether a node is worth deep-diving — i.e. first-party code the team owns.
|
|
44
|
+
* True when the node carries a repo `path`, or its type is a code container
|
|
45
|
+
* (service / frontend / gateway). People, external SaaS, and managed infra
|
|
46
|
+
* (database / cache / queue / storage) have nothing in the repo to investigate.
|
|
47
|
+
*/
|
|
48
|
+
declare function isDeepDivable(node: Pick<ArchitectureNode, "path" | "meta">): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Build the prompt a user copies into a coding agent to enrich one component's
|
|
51
|
+
* description. It invokes the skill's deep-dive via its slash command, keyed on
|
|
52
|
+
* the node `id` (unambiguous) — it deliberately does not restate how to deep-dive,
|
|
53
|
+
* since the architecture-docs skill already owns that (which files to read,
|
|
54
|
+
* dependencies to trace, the target description file, prose-only, etc.).
|
|
55
|
+
*/
|
|
56
|
+
declare function buildDeepDivePrompt(node: Pick<ArchitectureNode, "id">): string;
|
|
57
|
+
|
|
58
|
+
interface ApiHealthResponse {
|
|
59
|
+
status: "ok";
|
|
60
|
+
uptime: number;
|
|
61
|
+
timestamp: string;
|
|
62
|
+
}
|
|
63
|
+
type DiagramDirection = "TB" | "LR";
|
|
64
|
+
type DiagramLevel = 1 | 2 | 3;
|
|
65
|
+
type SourceHost = "github" | "gitlab" | "bitbucket";
|
|
66
|
+
type NodeMetaType = "system" | "person" | "service" | "database" | "queue" | "gateway" | "frontend" | "cache" | "storage" | "external";
|
|
67
|
+
type EdgeMetaType = "calls" | "reads" | "writes" | "publishes" | "subscribes" | "data-flow";
|
|
68
|
+
interface NodeMeta {
|
|
69
|
+
type?: NodeMetaType;
|
|
70
|
+
technology?: string;
|
|
71
|
+
isContainer?: boolean;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}
|
|
74
|
+
interface EdgeMeta {
|
|
75
|
+
type?: EdgeMetaType;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
interface ArchitectureNode {
|
|
79
|
+
id: string;
|
|
80
|
+
label: string;
|
|
81
|
+
parentId?: string | null;
|
|
82
|
+
subDiagramId?: string | null;
|
|
83
|
+
/** Repo-root-relative path to the file or directory this node maps to. Trailing "/" = directory. */
|
|
84
|
+
path?: string;
|
|
85
|
+
meta?: NodeMeta;
|
|
86
|
+
}
|
|
87
|
+
interface ArchitectureEdge {
|
|
88
|
+
id: string;
|
|
89
|
+
source: string;
|
|
90
|
+
target: string;
|
|
91
|
+
label?: string;
|
|
92
|
+
meta?: EdgeMeta;
|
|
93
|
+
}
|
|
94
|
+
interface DiagramFile {
|
|
95
|
+
name: string;
|
|
96
|
+
level?: DiagramLevel;
|
|
97
|
+
meta?: {
|
|
98
|
+
direction?: DiagramDirection;
|
|
99
|
+
layout?: string;
|
|
100
|
+
};
|
|
101
|
+
nodes: ArchitectureNode[];
|
|
102
|
+
edges?: ArchitectureEdge[];
|
|
103
|
+
}
|
|
104
|
+
interface ManifestFile {
|
|
105
|
+
name: string;
|
|
106
|
+
description?: string;
|
|
107
|
+
source?: string;
|
|
108
|
+
sourceHost?: SourceHost;
|
|
109
|
+
topDiagram: string;
|
|
110
|
+
diagrams: string[];
|
|
111
|
+
}
|
|
112
|
+
interface ApiDiagramSummary {
|
|
113
|
+
slug: string;
|
|
114
|
+
name: string;
|
|
115
|
+
level?: DiagramLevel;
|
|
116
|
+
nodeCount: number;
|
|
117
|
+
edgeCount: number;
|
|
118
|
+
}
|
|
119
|
+
interface ApiArchitectureSummary {
|
|
120
|
+
name: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
source?: string;
|
|
123
|
+
sourceHost?: SourceHost;
|
|
124
|
+
topDiagram: string;
|
|
125
|
+
diagrams: ApiDiagramSummary[];
|
|
126
|
+
}
|
|
127
|
+
interface ApiDiagram extends DiagramFile {
|
|
128
|
+
slug: string;
|
|
129
|
+
edges: ArchitectureEdge[];
|
|
130
|
+
}
|
|
131
|
+
interface ApiDiagramNodes {
|
|
132
|
+
diagramId: string;
|
|
133
|
+
nodes: ArchitectureNode[];
|
|
134
|
+
}
|
|
135
|
+
interface ApiDiagramEdges {
|
|
136
|
+
diagramId: string;
|
|
137
|
+
edges: ArchitectureEdge[];
|
|
138
|
+
}
|
|
139
|
+
interface ApiNodeDetail extends ArchitectureNode {
|
|
140
|
+
diagramId: string;
|
|
141
|
+
description: string;
|
|
142
|
+
}
|
|
143
|
+
interface NodeLayoutEntry {
|
|
144
|
+
x: number;
|
|
145
|
+
y: number;
|
|
146
|
+
width?: number;
|
|
147
|
+
height?: number;
|
|
148
|
+
}
|
|
149
|
+
interface DiagramLayoutFile {
|
|
150
|
+
version: 1;
|
|
151
|
+
diagramId: string;
|
|
152
|
+
updatedAt: string;
|
|
153
|
+
nodes: Record<string, NodeLayoutEntry>;
|
|
154
|
+
}
|
|
155
|
+
interface ApiDiagramLayout extends DiagramLayoutFile {
|
|
156
|
+
}
|
|
157
|
+
interface ApiDiagramLayoutUpdate {
|
|
158
|
+
nodes: Record<string, NodeLayoutEntry>;
|
|
159
|
+
}
|
|
160
|
+
type ApiArchitectureErrorCode = "diagram_not_found" | "node_not_found" | "description_not_found" | "architecture_unreadable" | "layout_invalid" | "layout_not_supported";
|
|
161
|
+
interface ApiArchitectureError {
|
|
162
|
+
error: ApiArchitectureErrorCode;
|
|
163
|
+
message?: string;
|
|
164
|
+
slug?: string;
|
|
165
|
+
id?: string;
|
|
166
|
+
}
|
|
167
|
+
type TectureRequest = {
|
|
168
|
+
id: string;
|
|
169
|
+
type: "loadSummary";
|
|
170
|
+
} | {
|
|
171
|
+
id: string;
|
|
172
|
+
type: "loadDiagram";
|
|
173
|
+
slug: string;
|
|
174
|
+
} | {
|
|
175
|
+
id: string;
|
|
176
|
+
type: "loadLayout";
|
|
177
|
+
slug: string;
|
|
178
|
+
} | {
|
|
179
|
+
id: string;
|
|
180
|
+
type: "loadNodeDetail";
|
|
181
|
+
nodeId: string;
|
|
182
|
+
} | {
|
|
183
|
+
id: string;
|
|
184
|
+
type: "openFile";
|
|
185
|
+
path: string;
|
|
186
|
+
} | {
|
|
187
|
+
id: string;
|
|
188
|
+
type: "saveLayout";
|
|
189
|
+
slug: string;
|
|
190
|
+
update: ApiDiagramLayoutUpdate;
|
|
191
|
+
};
|
|
192
|
+
type TectureResponse = {
|
|
193
|
+
id: string;
|
|
194
|
+
ok: true;
|
|
195
|
+
data: unknown;
|
|
196
|
+
} | {
|
|
197
|
+
id: string;
|
|
198
|
+
ok: false;
|
|
199
|
+
error: string;
|
|
200
|
+
};
|
|
201
|
+
type TectureEvent = {
|
|
202
|
+
type: "refresh";
|
|
203
|
+
} | {
|
|
204
|
+
type: "selectDiagram";
|
|
205
|
+
slug: string;
|
|
206
|
+
};
|
|
207
|
+
type TectureNotification = {
|
|
208
|
+
type: "ready";
|
|
209
|
+
} | {
|
|
210
|
+
type: "diagramChanged";
|
|
211
|
+
slug: string;
|
|
212
|
+
} | {
|
|
213
|
+
type: "usage";
|
|
214
|
+
event: string;
|
|
215
|
+
level?: number;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export { type ApiArchitectureError, type ApiArchitectureErrorCode, type ApiArchitectureSummary, type ApiDiagram, type ApiDiagramEdges, type ApiDiagramLayout, type ApiDiagramLayoutUpdate, type ApiDiagramNodes, type ApiDiagramSummary, type ApiHealthResponse, type ApiNodeDetail, type ArchitectureDataSource, type ArchitectureEdge, type ArchitectureNode, DescriptionNotFoundError, type DiagramDirection, type DiagramFile, type DiagramLayoutFile, type DiagramLevel, DiagramNotFoundError, type EdgeMeta, type EdgeMetaType, LayoutInvalidError, type LayoutStore, type ManifestFile, type NodeLayoutEntry, type NodeMeta, type NodeMetaType, NodeNotFoundError, SLUG_RE, type SourceHost, type TectureEvent, type TectureNotification, type TectureRequest, type TectureResponse, buildArchitectureSummary, buildDeepDivePrompt, buildSourceUrl, emptyLayout, findNode, isDeepDivable, isFiniteNumber, isValidLayoutEntry, normalizeLayoutUpdate };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/validators.ts
|
|
2
|
+
var SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
3
|
+
var DiagramNotFoundError = class extends Error {
|
|
4
|
+
constructor(slug) {
|
|
5
|
+
super(`Diagram not found: ${slug}`);
|
|
6
|
+
this.slug = slug;
|
|
7
|
+
this.name = "DiagramNotFoundError";
|
|
8
|
+
}
|
|
9
|
+
slug;
|
|
10
|
+
};
|
|
11
|
+
var NodeNotFoundError = class extends Error {
|
|
12
|
+
constructor(id) {
|
|
13
|
+
super(`Node not found: ${id}`);
|
|
14
|
+
this.id = id;
|
|
15
|
+
this.name = "NodeNotFoundError";
|
|
16
|
+
}
|
|
17
|
+
id;
|
|
18
|
+
};
|
|
19
|
+
var DescriptionNotFoundError = class extends Error {
|
|
20
|
+
constructor(id) {
|
|
21
|
+
super(`Description not found: ${id}`);
|
|
22
|
+
this.id = id;
|
|
23
|
+
this.name = "DescriptionNotFoundError";
|
|
24
|
+
}
|
|
25
|
+
id;
|
|
26
|
+
};
|
|
27
|
+
var LayoutInvalidError = class extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "LayoutInvalidError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
function isFiniteNumber(v) {
|
|
34
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
35
|
+
}
|
|
36
|
+
function isValidLayoutEntry(v) {
|
|
37
|
+
if (!v || typeof v !== "object") return false;
|
|
38
|
+
const e = v;
|
|
39
|
+
if (!isFiniteNumber(e.x) || !isFiniteNumber(e.y)) return false;
|
|
40
|
+
if ("width" in e && e.width !== void 0 && !isFiniteNumber(e.width)) return false;
|
|
41
|
+
if ("height" in e && e.height !== void 0 && !isFiniteNumber(e.height)) return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
function emptyLayout(slug) {
|
|
45
|
+
return { version: 1, diagramId: slug, updatedAt: "", nodes: {} };
|
|
46
|
+
}
|
|
47
|
+
function normalizeLayoutUpdate(slug, update) {
|
|
48
|
+
if (!SLUG_RE.test(slug)) {
|
|
49
|
+
throw new LayoutInvalidError(`invalid diagram slug: ${slug}`);
|
|
50
|
+
}
|
|
51
|
+
if (!update || typeof update !== "object" || !update.nodes || typeof update.nodes !== "object") {
|
|
52
|
+
throw new LayoutInvalidError(
|
|
53
|
+
"body must be { nodes: Record<string, { x, y, width?, height? }> }"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const nodes = {};
|
|
57
|
+
for (const [id, entry] of Object.entries(update.nodes)) {
|
|
58
|
+
if (!SLUG_RE.test(id)) {
|
|
59
|
+
throw new LayoutInvalidError(`invalid node id: ${id}`);
|
|
60
|
+
}
|
|
61
|
+
if (!isValidLayoutEntry(entry)) {
|
|
62
|
+
throw new LayoutInvalidError(`invalid layout entry for ${id}`);
|
|
63
|
+
}
|
|
64
|
+
const out = { x: entry.x, y: entry.y };
|
|
65
|
+
if (isFiniteNumber(entry.width)) out.width = entry.width;
|
|
66
|
+
if (isFiniteNumber(entry.height)) out.height = entry.height;
|
|
67
|
+
nodes[id] = out;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
version: 1,
|
|
71
|
+
diagramId: slug,
|
|
72
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
73
|
+
nodes
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function buildArchitectureSummary(source) {
|
|
77
|
+
const manifest = await source.loadManifest();
|
|
78
|
+
const diagrams = await Promise.all(
|
|
79
|
+
manifest.diagrams.map(async (slug) => {
|
|
80
|
+
const diagram = await source.loadDiagram(slug);
|
|
81
|
+
return {
|
|
82
|
+
slug,
|
|
83
|
+
name: diagram.name,
|
|
84
|
+
level: diagram.level,
|
|
85
|
+
nodeCount: diagram.nodes?.length ?? 0,
|
|
86
|
+
edgeCount: diagram.edges?.length ?? 0
|
|
87
|
+
};
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
return {
|
|
91
|
+
name: manifest.name,
|
|
92
|
+
description: manifest.description,
|
|
93
|
+
source: manifest.source,
|
|
94
|
+
sourceHost: manifest.sourceHost,
|
|
95
|
+
topDiagram: manifest.topDiagram,
|
|
96
|
+
diagrams
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
var HOST_DOMAINS = [
|
|
100
|
+
{ host: "github", match: /(^|\.)github\.com$/i },
|
|
101
|
+
{ host: "gitlab", match: /(^|\.)gitlab\.com$/i },
|
|
102
|
+
{ host: "bitbucket", match: /(^|\.)bitbucket\.org$/i }
|
|
103
|
+
];
|
|
104
|
+
function inferSourceHost(source) {
|
|
105
|
+
const hostname = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/]*@)?([^:/?#]+)/i.exec(source)?.[1];
|
|
106
|
+
if (!hostname) return void 0;
|
|
107
|
+
return HOST_DOMAINS.find(({ match }) => match.test(hostname))?.host;
|
|
108
|
+
}
|
|
109
|
+
function buildSourceUrl(source, host, path) {
|
|
110
|
+
if (!source) return void 0;
|
|
111
|
+
const base = source.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
112
|
+
const resolvedHost = host ?? inferSourceHost(base);
|
|
113
|
+
const isDir = path.endsWith("/");
|
|
114
|
+
const encoded = path.replace(/^\/+/, "").replace(/\/+$/, "").split("/").map(encodeURIComponent).join("/");
|
|
115
|
+
if (!encoded) return base;
|
|
116
|
+
switch (resolvedHost) {
|
|
117
|
+
case "github":
|
|
118
|
+
return `${base}/${isDir ? "tree" : "blob"}/HEAD/${encoded}`;
|
|
119
|
+
case "gitlab":
|
|
120
|
+
return `${base}/-/${isDir ? "tree" : "blob"}/HEAD/${encoded}`;
|
|
121
|
+
case "bitbucket":
|
|
122
|
+
return `${base}/src/HEAD/${encoded}`;
|
|
123
|
+
default:
|
|
124
|
+
return base;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function findNode(source, nodeId) {
|
|
128
|
+
if (!SLUG_RE.test(nodeId)) throw new NodeNotFoundError(nodeId);
|
|
129
|
+
const manifest = await source.loadManifest();
|
|
130
|
+
for (const slug of manifest.diagrams) {
|
|
131
|
+
let diagram;
|
|
132
|
+
try {
|
|
133
|
+
diagram = await source.loadDiagram(slug);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err instanceof DiagramNotFoundError) continue;
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
const match = diagram.nodes?.find((n) => n.id === nodeId);
|
|
139
|
+
if (match) return { node: match, diagramId: slug };
|
|
140
|
+
}
|
|
141
|
+
throw new NodeNotFoundError(nodeId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/deepDive.ts
|
|
145
|
+
var CODE_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
146
|
+
"service",
|
|
147
|
+
"frontend",
|
|
148
|
+
"gateway"
|
|
149
|
+
]);
|
|
150
|
+
function isDeepDivable(node) {
|
|
151
|
+
if (node.path) return true;
|
|
152
|
+
const type = node.meta?.type;
|
|
153
|
+
return type !== void 0 && CODE_NODE_TYPES.has(type);
|
|
154
|
+
}
|
|
155
|
+
function buildDeepDivePrompt(node) {
|
|
156
|
+
return `/architecture-docs deep-dive ${node.id}`;
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
DescriptionNotFoundError,
|
|
160
|
+
DiagramNotFoundError,
|
|
161
|
+
LayoutInvalidError,
|
|
162
|
+
NodeNotFoundError,
|
|
163
|
+
SLUG_RE,
|
|
164
|
+
buildArchitectureSummary,
|
|
165
|
+
buildDeepDivePrompt,
|
|
166
|
+
buildSourceUrl,
|
|
167
|
+
emptyLayout,
|
|
168
|
+
findNode,
|
|
169
|
+
isDeepDivable,
|
|
170
|
+
isFiniteNumber,
|
|
171
|
+
isValidLayoutEntry,
|
|
172
|
+
normalizeLayoutUpdate
|
|
173
|
+
};
|
|
174
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/validators.ts","../src/deepDive.ts"],"sourcesContent":["import type {\n ApiArchitectureSummary,\n ApiDiagramLayoutUpdate,\n ApiDiagramSummary,\n ArchitectureNode,\n DiagramFile,\n DiagramLayoutFile,\n ManifestFile,\n NodeLayoutEntry,\n SourceHost,\n} from \"./index\";\n\nexport const SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nexport class DiagramNotFoundError extends Error {\n constructor(public readonly slug: string) {\n super(`Diagram not found: ${slug}`);\n this.name = \"DiagramNotFoundError\";\n }\n}\n\nexport class NodeNotFoundError extends Error {\n constructor(public readonly id: string) {\n super(`Node not found: ${id}`);\n this.name = \"NodeNotFoundError\";\n }\n}\n\nexport class DescriptionNotFoundError extends Error {\n constructor(public readonly id: string) {\n super(`Description not found: ${id}`);\n this.name = \"DescriptionNotFoundError\";\n }\n}\n\nexport class LayoutInvalidError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"LayoutInvalidError\";\n }\n}\n\nexport interface ArchitectureDataSource {\n loadManifest(): Promise<ManifestFile>;\n loadDiagram(slug: string): Promise<DiagramFile>;\n loadDescription(nodeId: string): Promise<string>;\n}\n\nexport interface LayoutStore {\n loadLayout(slug: string): Promise<DiagramLayoutFile>;\n saveLayout(\n slug: string,\n update: ApiDiagramLayoutUpdate,\n ): Promise<DiagramLayoutFile>;\n}\n\nexport function isFiniteNumber(v: unknown): v is number {\n return typeof v === \"number\" && Number.isFinite(v);\n}\n\nexport function isValidLayoutEntry(v: unknown): v is NodeLayoutEntry {\n if (!v || typeof v !== \"object\") return false;\n const e = v as Record<string, unknown>;\n if (!isFiniteNumber(e.x) || !isFiniteNumber(e.y)) return false;\n if (\"width\" in e && e.width !== undefined && !isFiniteNumber(e.width)) return false;\n if (\"height\" in e && e.height !== undefined && !isFiniteNumber(e.height)) return false;\n return true;\n}\n\nexport function emptyLayout(slug: string): DiagramLayoutFile {\n return { version: 1, diagramId: slug, updatedAt: \"\", nodes: {} };\n}\n\nexport function normalizeLayoutUpdate(\n slug: string,\n update: ApiDiagramLayoutUpdate,\n): DiagramLayoutFile {\n if (!SLUG_RE.test(slug)) {\n throw new LayoutInvalidError(`invalid diagram slug: ${slug}`);\n }\n if (\n !update ||\n typeof update !== \"object\" ||\n !update.nodes ||\n typeof update.nodes !== \"object\"\n ) {\n throw new LayoutInvalidError(\n \"body must be { nodes: Record<string, { x, y, width?, height? }> }\",\n );\n }\n const nodes: Record<string, NodeLayoutEntry> = {};\n for (const [id, entry] of Object.entries(update.nodes)) {\n if (!SLUG_RE.test(id)) {\n throw new LayoutInvalidError(`invalid node id: ${id}`);\n }\n if (!isValidLayoutEntry(entry)) {\n throw new LayoutInvalidError(`invalid layout entry for ${id}`);\n }\n const out: NodeLayoutEntry = { x: entry.x, y: entry.y };\n if (isFiniteNumber(entry.width)) out.width = entry.width;\n if (isFiniteNumber(entry.height)) out.height = entry.height;\n nodes[id] = out;\n }\n return {\n version: 1,\n diagramId: slug,\n updatedAt: new Date().toISOString(),\n nodes,\n };\n}\n\nexport async function buildArchitectureSummary(\n source: ArchitectureDataSource,\n): Promise<ApiArchitectureSummary> {\n const manifest = await source.loadManifest();\n const diagrams = await Promise.all(\n manifest.diagrams.map(async (slug): Promise<ApiDiagramSummary> => {\n const diagram = await source.loadDiagram(slug);\n return {\n slug,\n name: diagram.name,\n level: diagram.level,\n nodeCount: diagram.nodes?.length ?? 0,\n edgeCount: diagram.edges?.length ?? 0,\n };\n }),\n );\n return {\n name: manifest.name,\n description: manifest.description,\n source: manifest.source,\n sourceHost: manifest.sourceHost,\n topDiagram: manifest.topDiagram,\n diagrams,\n };\n}\n\nconst HOST_DOMAINS: Array<{ host: SourceHost; match: RegExp }> = [\n { host: \"github\", match: /(^|\\.)github\\.com$/i },\n { host: \"gitlab\", match: /(^|\\.)gitlab\\.com$/i },\n { host: \"bitbucket\", match: /(^|\\.)bitbucket\\.org$/i },\n];\n\nfunction inferSourceHost(source: string): SourceHost | undefined {\n // Extract the hostname without relying on the URL global (kept lib-agnostic for shared):\n // scheme://[userinfo@]host[:port]/...\n const hostname = /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^@/]*@)?([^:/?#]+)/i.exec(source)?.[1];\n if (!hostname) return undefined;\n return HOST_DOMAINS.find(({ match }) => match.test(hostname))?.host;\n}\n\n/**\n * Build a web URL that views `path` (a repo-root-relative file or directory, trailing \"/\" = dir)\n * on the source repo host. `HEAD` resolves to the default branch so links stay current. Falls back\n * to the repo root when the host is unknown, and returns undefined when there is no source.\n */\nexport function buildSourceUrl(\n source: string | undefined,\n host: SourceHost | undefined,\n path: string,\n): string | undefined {\n if (!source) return undefined;\n const base = source.replace(/\\/+$/, \"\").replace(/\\.git$/i, \"\");\n const resolvedHost = host ?? inferSourceHost(base);\n const isDir = path.endsWith(\"/\");\n const encoded = path\n .replace(/^\\/+/, \"\")\n .replace(/\\/+$/, \"\")\n .split(\"/\")\n .map(encodeURIComponent)\n .join(\"/\");\n if (!encoded) return base;\n switch (resolvedHost) {\n case \"github\":\n return `${base}/${isDir ? \"tree\" : \"blob\"}/HEAD/${encoded}`;\n case \"gitlab\":\n return `${base}/-/${isDir ? \"tree\" : \"blob\"}/HEAD/${encoded}`;\n case \"bitbucket\":\n return `${base}/src/HEAD/${encoded}`;\n default:\n return base;\n }\n}\n\nexport async function findNode(\n source: ArchitectureDataSource,\n nodeId: string,\n): Promise<{ node: ArchitectureNode; diagramId: string }> {\n if (!SLUG_RE.test(nodeId)) throw new NodeNotFoundError(nodeId);\n const manifest = await source.loadManifest();\n for (const slug of manifest.diagrams) {\n let diagram: DiagramFile;\n try {\n diagram = await source.loadDiagram(slug);\n } catch (err) {\n if (err instanceof DiagramNotFoundError) continue;\n throw err;\n }\n const match = diagram.nodes?.find((n) => n.id === nodeId);\n if (match) return { node: match, diagramId: slug };\n }\n throw new NodeNotFoundError(nodeId);\n}\n","import type { ArchitectureNode, NodeMetaType } from \"./index\";\n\n/** Node types that map to first-party code worth a deep-dive. */\nconst CODE_NODE_TYPES: ReadonlySet<NodeMetaType> = new Set([\n \"service\",\n \"frontend\",\n \"gateway\",\n]);\n\n/**\n * Whether a node is worth deep-diving — i.e. first-party code the team owns.\n * True when the node carries a repo `path`, or its type is a code container\n * (service / frontend / gateway). People, external SaaS, and managed infra\n * (database / cache / queue / storage) have nothing in the repo to investigate.\n */\nexport function isDeepDivable(\n node: Pick<ArchitectureNode, \"path\" | \"meta\">,\n): boolean {\n if (node.path) return true;\n const type = node.meta?.type;\n return type !== undefined && CODE_NODE_TYPES.has(type);\n}\n\n/**\n * Build the prompt a user copies into a coding agent to enrich one component's\n * description. It invokes the skill's deep-dive via its slash command, keyed on\n * the node `id` (unambiguous) — it deliberately does not restate how to deep-dive,\n * since the architecture-docs skill already owns that (which files to read,\n * dependencies to trace, the target description file, prose-only, etc.).\n */\nexport function buildDeepDivePrompt(\n node: Pick<ArchitectureNode, \"id\">,\n): string {\n return `/architecture-docs deep-dive ${node.id}`;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tecture/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared types and runtime helpers for Tecture — the file-based (JSON + Markdown) architecture documentation format.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"c4",
|
|
7
|
+
"architecture",
|
|
8
|
+
"diagram",
|
|
9
|
+
"documentation",
|
|
10
|
+
"tecture"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Shanika Wijerathna <shanikacj@gmail.com>",
|
|
14
|
+
"homepage": "https://github.com/tecture-io/tecture#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/tecture-io/tecture.git",
|
|
18
|
+
"directory": "packages/shared"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/tecture-io/tecture/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"CHANGELOG.md"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"tsup": "^8.3.5",
|
|
41
|
+
"typescript": "^5.6.3"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"typecheck": "tsc --noEmit"
|
|
46
|
+
}
|
|
47
|
+
}
|