@fairfox/polly 0.1.1 → 0.1.2
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/cli/polly.ts +9 -3
- package/package.json +2 -2
- package/vendor/analysis/src/extract/adr.ts +212 -0
- package/vendor/analysis/src/extract/architecture.ts +160 -0
- package/vendor/analysis/src/extract/contexts.ts +298 -0
- package/vendor/analysis/src/extract/flows.ts +309 -0
- package/vendor/analysis/src/extract/handlers.ts +321 -0
- package/vendor/analysis/src/extract/index.ts +9 -0
- package/vendor/analysis/src/extract/integrations.ts +329 -0
- package/vendor/analysis/src/extract/manifest.ts +298 -0
- package/vendor/analysis/src/extract/types.ts +389 -0
- package/vendor/analysis/src/index.ts +7 -0
- package/vendor/analysis/src/types/adr.ts +53 -0
- package/vendor/analysis/src/types/architecture.ts +245 -0
- package/vendor/analysis/src/types/core.ts +210 -0
- package/vendor/analysis/src/types/index.ts +18 -0
- package/vendor/verify/src/adapters/base.ts +164 -0
- package/vendor/verify/src/adapters/detection.ts +281 -0
- package/vendor/verify/src/adapters/event-bus/index.ts +480 -0
- package/vendor/verify/src/adapters/web-extension/index.ts +508 -0
- package/vendor/verify/src/adapters/websocket/index.ts +486 -0
- package/vendor/verify/src/cli.ts +430 -0
- package/vendor/verify/src/codegen/config.ts +354 -0
- package/vendor/verify/src/codegen/tla.ts +719 -0
- package/vendor/verify/src/config/parser.ts +303 -0
- package/vendor/verify/src/config/types.ts +113 -0
- package/vendor/verify/src/core/model.ts +267 -0
- package/vendor/verify/src/core/primitives.ts +106 -0
- package/vendor/verify/src/extract/handlers.ts +2 -0
- package/vendor/verify/src/extract/types.ts +2 -0
- package/vendor/verify/src/index.ts +150 -0
- package/vendor/verify/src/primitives/index.ts +102 -0
- package/vendor/verify/src/runner/docker.ts +283 -0
- package/vendor/verify/src/types.ts +51 -0
- package/vendor/visualize/src/cli.ts +365 -0
- package/vendor/visualize/src/codegen/structurizr.ts +770 -0
- package/vendor/visualize/src/index.ts +13 -0
- package/vendor/visualize/src/runner/export.ts +235 -0
- package/vendor/visualize/src/viewer/server.ts +485 -0
package/cli/polly.ts
CHANGED
|
@@ -111,7 +111,10 @@ async function dev() {
|
|
|
111
111
|
* Verify command - delegate to @fairfox/web-ext-verify
|
|
112
112
|
*/
|
|
113
113
|
async function verify() {
|
|
114
|
-
|
|
114
|
+
// Try vendor directory first (published package), then monorepo
|
|
115
|
+
const vendorCli = `${__dirname}/../vendor/verify/src/cli.ts`;
|
|
116
|
+
const monorepoCli = `${__dirname}/../../verify/src/cli.ts`;
|
|
117
|
+
const verifyCli = (await Bun.file(vendorCli).exists()) ? vendorCli : monorepoCli;
|
|
115
118
|
|
|
116
119
|
const proc = Bun.spawn(["bun", verifyCli, ...commandArgs], {
|
|
117
120
|
cwd,
|
|
@@ -127,10 +130,13 @@ async function verify() {
|
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
/**
|
|
130
|
-
* Visualize command - delegate to @fairfox/
|
|
133
|
+
* Visualize command - delegate to @fairfox/polly-visualize
|
|
131
134
|
*/
|
|
132
135
|
async function visualize() {
|
|
133
|
-
|
|
136
|
+
// Try vendor directory first (published package), then monorepo
|
|
137
|
+
const vendorCli = `${__dirname}/../vendor/visualize/src/cli.ts`;
|
|
138
|
+
const monorepoCli = `${__dirname}/../../visualize/src/cli.ts`;
|
|
139
|
+
const visualizeCli = (await Bun.file(vendorCli).exists()) ? vendorCli : monorepoCli;
|
|
134
140
|
|
|
135
141
|
const proc = Bun.spawn(["bun", visualizeCli, ...commandArgs], {
|
|
136
142
|
cwd,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fairfox/polly",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Multi-execution-context framework with reactive state and cross-context messaging for Chrome extensions, PWAs, and worker-based applications",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"types": "./dist/shared/types/messages.d.ts"
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
-
"files": ["dist", "README.md", "LICENSE"],
|
|
47
|
+
"files": ["dist", "cli", "vendor", "README.md", "LICENSE"],
|
|
48
48
|
"scripts": {
|
|
49
49
|
"dev": "bun run build.ts --watch",
|
|
50
50
|
"build": "bun run build.ts",
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// ADR (Architecture Decision Record) extractor
|
|
2
|
+
// Parses ADRs from markdown files following Michael Nygard's format
|
|
3
|
+
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import type { ADR, ADRStatus, ADRLink, ADRCollection } from "../types/adr";
|
|
7
|
+
|
|
8
|
+
export class ADRExtractor {
|
|
9
|
+
private projectRoot: string;
|
|
10
|
+
|
|
11
|
+
constructor(projectRoot: string) {
|
|
12
|
+
this.projectRoot = projectRoot;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract ADRs from docs/adr directory
|
|
17
|
+
*/
|
|
18
|
+
extract(): ADRCollection {
|
|
19
|
+
const adrDir = this.findADRDirectory();
|
|
20
|
+
|
|
21
|
+
if (!adrDir || !fs.existsSync(adrDir)) {
|
|
22
|
+
return {
|
|
23
|
+
adrs: [],
|
|
24
|
+
directory: adrDir || path.join(this.projectRoot, "docs", "adr"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const files = fs
|
|
29
|
+
.readdirSync(adrDir)
|
|
30
|
+
.filter((file) => file.endsWith(".md"))
|
|
31
|
+
.map((file) => path.join(adrDir, file));
|
|
32
|
+
|
|
33
|
+
const adrs: ADR[] = [];
|
|
34
|
+
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
try {
|
|
37
|
+
const adr = this.parseADR(file);
|
|
38
|
+
if (adr) {
|
|
39
|
+
adrs.push(adr);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.warn(`Failed to parse ADR: ${file}`, error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Sort by ID
|
|
47
|
+
adrs.sort((a, b) => a.id.localeCompare(b.id));
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
adrs,
|
|
51
|
+
directory: adrDir,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Find ADR directory
|
|
57
|
+
*/
|
|
58
|
+
private findADRDirectory(): string | null {
|
|
59
|
+
const candidates = [
|
|
60
|
+
path.join(this.projectRoot, "docs", "adr"),
|
|
61
|
+
path.join(this.projectRoot, "docs", "architecture", "decisions"),
|
|
62
|
+
path.join(this.projectRoot, "adr"),
|
|
63
|
+
path.join(this.projectRoot, "architecture", "decisions"),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
if (fs.existsSync(candidate)) {
|
|
68
|
+
return candidate;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse ADR from markdown file
|
|
77
|
+
*/
|
|
78
|
+
private parseADR(filePath: string): ADR | null {
|
|
79
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
80
|
+
const fileName = path.basename(filePath, ".md");
|
|
81
|
+
|
|
82
|
+
// Extract ID from filename (e.g., "0001-use-preact.md" → "0001")
|
|
83
|
+
const idMatch = fileName.match(/^(\d+)/);
|
|
84
|
+
const id = idMatch ? idMatch[1] : fileName;
|
|
85
|
+
|
|
86
|
+
// Extract title from first heading
|
|
87
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
88
|
+
const title = titleMatch ? titleMatch[1].trim() : fileName;
|
|
89
|
+
|
|
90
|
+
// Extract status
|
|
91
|
+
const status = this.extractStatus(content);
|
|
92
|
+
|
|
93
|
+
// Extract date
|
|
94
|
+
const date = this.extractDate(content);
|
|
95
|
+
|
|
96
|
+
// Extract sections
|
|
97
|
+
const context = this.extractSection(content, "Context");
|
|
98
|
+
const decision = this.extractSection(content, "Decision");
|
|
99
|
+
const consequences = this.extractSection(content, "Consequences");
|
|
100
|
+
|
|
101
|
+
// Extract alternatives (if present)
|
|
102
|
+
const alternativesSection = this.extractSection(content, "Alternatives");
|
|
103
|
+
const alternatives = alternativesSection
|
|
104
|
+
? alternativesSection
|
|
105
|
+
.split("\n")
|
|
106
|
+
.filter((line) => line.trim().startsWith("-"))
|
|
107
|
+
.map((line) => line.replace(/^-\s*/, "").trim())
|
|
108
|
+
: undefined;
|
|
109
|
+
|
|
110
|
+
// Extract links
|
|
111
|
+
const links = this.extractLinks(content);
|
|
112
|
+
|
|
113
|
+
if (!context || !decision || !consequences) {
|
|
114
|
+
// Not a valid ADR format
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
title,
|
|
121
|
+
status,
|
|
122
|
+
date,
|
|
123
|
+
context,
|
|
124
|
+
decision,
|
|
125
|
+
consequences,
|
|
126
|
+
alternatives,
|
|
127
|
+
links,
|
|
128
|
+
source: filePath,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Extract status from content
|
|
134
|
+
*/
|
|
135
|
+
private extractStatus(content: string): ADRStatus {
|
|
136
|
+
const statusMatch = content.match(/Status:\s*(\w+)/i);
|
|
137
|
+
if (!statusMatch) return "accepted";
|
|
138
|
+
|
|
139
|
+
const status = statusMatch[1].toLowerCase();
|
|
140
|
+
if (["proposed", "accepted", "deprecated", "superseded"].includes(status)) {
|
|
141
|
+
return status as ADRStatus;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return "accepted";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Extract date from content
|
|
149
|
+
*/
|
|
150
|
+
private extractDate(content: string): string {
|
|
151
|
+
// Try to find date in various formats
|
|
152
|
+
const dateMatch =
|
|
153
|
+
content.match(/Date:\s*(\d{4}-\d{2}-\d{2})/i) || content.match(/(\d{4}-\d{2}-\d{2})/i);
|
|
154
|
+
|
|
155
|
+
return dateMatch ? dateMatch[1] : new Date().toISOString().split("T")[0];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extract section content
|
|
160
|
+
*/
|
|
161
|
+
private extractSection(content: string, sectionName: string): string {
|
|
162
|
+
// Match section heading and capture content until next heading or end
|
|
163
|
+
const regex = new RegExp(`##\\s+${sectionName}\\s*\\n([\\s\\S]*?)(?=\\n##|$)`, "i");
|
|
164
|
+
const match = content.match(regex);
|
|
165
|
+
|
|
166
|
+
return match ? match[1].trim() : "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract links to other ADRs
|
|
171
|
+
*/
|
|
172
|
+
private extractLinks(content: string): ADRLink[] {
|
|
173
|
+
const links: ADRLink[] = [];
|
|
174
|
+
|
|
175
|
+
// Look for supersedes/superseded-by links
|
|
176
|
+
const supersedesMatch = content.match(/Supersedes:\s*ADR-(\d+)/gi);
|
|
177
|
+
if (supersedesMatch) {
|
|
178
|
+
for (const match of supersedesMatch) {
|
|
179
|
+
const idMatch = match.match(/ADR-(\d+)/);
|
|
180
|
+
if (idMatch) {
|
|
181
|
+
links.push({
|
|
182
|
+
type: "supersedes",
|
|
183
|
+
adrId: idMatch[1],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const supersededByMatch = content.match(/Superseded by:\s*ADR-(\d+)/gi);
|
|
190
|
+
if (supersededByMatch) {
|
|
191
|
+
for (const match of supersededByMatch) {
|
|
192
|
+
const idMatch = match.match(/ADR-(\d+)/);
|
|
193
|
+
if (idMatch) {
|
|
194
|
+
links.push({
|
|
195
|
+
type: "superseded-by",
|
|
196
|
+
adrId: idMatch[1],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return links;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Extract ADRs from project
|
|
208
|
+
*/
|
|
209
|
+
export function extractADRs(projectRoot: string): ADRCollection {
|
|
210
|
+
const extractor = new ADRExtractor(projectRoot);
|
|
211
|
+
return extractor.extract();
|
|
212
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Architecture analysis - main orchestrator
|
|
2
|
+
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { ArchitectureAnalysis, ContextInfo } from "../types/architecture";
|
|
6
|
+
import { ManifestParser } from "./manifest";
|
|
7
|
+
import { ContextAnalyzer } from "./contexts";
|
|
8
|
+
import { FlowAnalyzer } from "./flows";
|
|
9
|
+
import { IntegrationAnalyzer } from "./integrations";
|
|
10
|
+
import { HandlerExtractor } from "./handlers";
|
|
11
|
+
import { extractADRs } from "./adr";
|
|
12
|
+
|
|
13
|
+
export interface ArchitectureAnalysisOptions {
|
|
14
|
+
/** Path to tsconfig.json */
|
|
15
|
+
tsConfigPath: string;
|
|
16
|
+
|
|
17
|
+
/** Project root directory (where manifest.json is) */
|
|
18
|
+
projectRoot: string;
|
|
19
|
+
|
|
20
|
+
/** Optional: Override detected contexts */
|
|
21
|
+
contexts?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Comprehensive architecture analyzer
|
|
26
|
+
*/
|
|
27
|
+
export class ArchitectureAnalyzer {
|
|
28
|
+
private options: ArchitectureAnalysisOptions;
|
|
29
|
+
|
|
30
|
+
constructor(options: ArchitectureAnalysisOptions) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Perform complete architecture analysis
|
|
36
|
+
*/
|
|
37
|
+
async analyze(): Promise<ArchitectureAnalysis> {
|
|
38
|
+
// 1. Parse manifest.json
|
|
39
|
+
const manifestParser = new ManifestParser(this.options.projectRoot);
|
|
40
|
+
const manifest = manifestParser.parse();
|
|
41
|
+
const entryPoints = manifestParser.getContextEntryPoints();
|
|
42
|
+
|
|
43
|
+
// 2. Extract message handlers
|
|
44
|
+
const handlerExtractor = new HandlerExtractor(this.options.tsConfigPath);
|
|
45
|
+
const { handlers } = handlerExtractor.extractHandlers();
|
|
46
|
+
|
|
47
|
+
// 3. Analyze each context
|
|
48
|
+
const contextAnalyzer = new ContextAnalyzer(this.options.tsConfigPath);
|
|
49
|
+
const contexts: Record<string, ContextInfo> = {};
|
|
50
|
+
|
|
51
|
+
for (const [contextType, entryPoint] of Object.entries(entryPoints)) {
|
|
52
|
+
try {
|
|
53
|
+
const contextInfo = contextAnalyzer.analyzeContext(contextType, entryPoint, handlers);
|
|
54
|
+
contexts[contextType] = contextInfo;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.warn(`Failed to analyze context ${contextType}: ${error}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 4. Analyze message flows
|
|
61
|
+
const flowAnalyzer = new FlowAnalyzer(this.options.tsConfigPath, handlers);
|
|
62
|
+
const messageFlows = flowAnalyzer.analyzeFlows();
|
|
63
|
+
|
|
64
|
+
// 5. Analyze external integrations
|
|
65
|
+
const integrationAnalyzer = new IntegrationAnalyzer(this.options.tsConfigPath);
|
|
66
|
+
const integrations = integrationAnalyzer.analyzeIntegrations();
|
|
67
|
+
|
|
68
|
+
// 6. Merge external API calls into context info
|
|
69
|
+
this.mergeExternalAPIsIntoContexts(contexts, integrations);
|
|
70
|
+
|
|
71
|
+
// 7. Extract ADRs (Architecture Decision Records)
|
|
72
|
+
const adrs = extractADRs(this.options.projectRoot);
|
|
73
|
+
|
|
74
|
+
// 8. Extract repository info
|
|
75
|
+
const repository = this.extractRepositoryInfo();
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
system: {
|
|
79
|
+
name: manifest.name,
|
|
80
|
+
version: manifest.version,
|
|
81
|
+
description: manifest.description,
|
|
82
|
+
},
|
|
83
|
+
manifest,
|
|
84
|
+
contexts,
|
|
85
|
+
messageFlows,
|
|
86
|
+
integrations,
|
|
87
|
+
adrs: adrs.adrs.length > 0 ? adrs : undefined,
|
|
88
|
+
repository,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Merge external API calls into their respective contexts
|
|
94
|
+
*/
|
|
95
|
+
private mergeExternalAPIsIntoContexts(
|
|
96
|
+
contexts: Record<string, ContextInfo>,
|
|
97
|
+
integrations: ExternalIntegration[]
|
|
98
|
+
): void {
|
|
99
|
+
for (const integration of integrations) {
|
|
100
|
+
if (integration.calls) {
|
|
101
|
+
for (const call of integration.calls) {
|
|
102
|
+
// Find which context this call belongs to
|
|
103
|
+
for (const [contextType, contextInfo] of Object.entries(contexts)) {
|
|
104
|
+
if (call.location.file.includes(`/${contextType}/`)) {
|
|
105
|
+
contextInfo.externalAPIs.push(call);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract repository information from package.json
|
|
116
|
+
*/
|
|
117
|
+
private extractRepositoryInfo(): ArchitectureAnalysis["repository"] {
|
|
118
|
+
const packageJsonPath = path.join(this.options.projectRoot, "package.json");
|
|
119
|
+
|
|
120
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const content = fs.readFileSync(packageJsonPath, "utf-8");
|
|
126
|
+
const packageJson = JSON.parse(content);
|
|
127
|
+
|
|
128
|
+
if (packageJson.repository) {
|
|
129
|
+
if (typeof packageJson.repository === "string") {
|
|
130
|
+
return {
|
|
131
|
+
url: packageJson.repository,
|
|
132
|
+
type: "git",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
url: packageJson.repository.url || packageJson.repository,
|
|
138
|
+
type: packageJson.repository.type || "git",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn(`Failed to parse package.json: ${error}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Convenience function to analyze architecture
|
|
151
|
+
*/
|
|
152
|
+
export async function analyzeArchitecture(
|
|
153
|
+
options: ArchitectureAnalysisOptions
|
|
154
|
+
): Promise<ArchitectureAnalysis> {
|
|
155
|
+
const analyzer = new ArchitectureAnalyzer(options);
|
|
156
|
+
return analyzer.analyze();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Fix: Import ExternalIntegration type
|
|
160
|
+
import type { ExternalIntegration } from "../types/architecture";
|