@interchained/portal-agent 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/dist/aiassist.d.ts +43 -0
- package/dist/aiassist.d.ts.map +1 -0
- package/dist/aiassist.js +87 -0
- package/dist/aiassist.js.map +1 -0
- package/dist/audit.d.ts +37 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +233 -0
- package/dist/audit.js.map +1 -0
- package/dist/generate.d.ts +41 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +92 -0
- package/dist/generate.js.map +1 -0
- package/dist/guard.d.ts +30 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +106 -0
- package/dist/guard.js.map +1 -0
- package/dist/improve.d.ts +37 -0
- package/dist/improve.d.ts.map +1 -0
- package/dist/improve.js +85 -0
- package/dist/improve.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/patch.d.ts +38 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +97 -0
- package/dist/patch.js.map +1 -0
- package/dist/runner.d.ts +40 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +81 -0
- package/dist/runner.js.map +1 -0
- package/dist/sentinel.d.ts +35 -0
- package/dist/sentinel.d.ts.map +1 -0
- package/dist/sentinel.js +75 -0
- package/dist/sentinel.js.map +1 -0
- package/package.json +32 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AiAssist.net HTTP client.
|
|
3
|
+
* Uses an OpenAI-compatible chat completions API.
|
|
4
|
+
* Set AIASSIST_API_KEY or VITE_AIAS_API_KEY in your environment.
|
|
5
|
+
*/
|
|
6
|
+
export interface Message {
|
|
7
|
+
role: "system" | "user" | "assistant";
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CompletionOptions {
|
|
11
|
+
temperature?: number;
|
|
12
|
+
maxTokens?: number;
|
|
13
|
+
/** Stop sequences */
|
|
14
|
+
stop?: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare class AiAssistError extends Error {
|
|
17
|
+
readonly statusCode: number;
|
|
18
|
+
readonly body?: string | undefined;
|
|
19
|
+
constructor(message: string, statusCode: number, body?: string | undefined);
|
|
20
|
+
}
|
|
21
|
+
export interface AiAssistConfig {
|
|
22
|
+
apiKey: string;
|
|
23
|
+
/** Base URL for the AiAssist API. Defaults to https://api.aiassist.net/v1 */
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
/** Model identifier */
|
|
26
|
+
model?: string;
|
|
27
|
+
/** Request timeout in ms. Defaults to 60 000 */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class AiAssistClient {
|
|
31
|
+
private readonly apiKey;
|
|
32
|
+
private readonly baseUrl;
|
|
33
|
+
private readonly model;
|
|
34
|
+
private readonly timeoutMs;
|
|
35
|
+
constructor(config: AiAssistConfig);
|
|
36
|
+
/** Single-turn completion — convenience wrapper around chat() */
|
|
37
|
+
complete(prompt: string, systemPrompt?: string, options?: CompletionOptions): Promise<string>;
|
|
38
|
+
/** Multi-turn chat completion */
|
|
39
|
+
chat(messages: Message[], options?: CompletionOptions): Promise<string>;
|
|
40
|
+
/** Resolve API key from environment if not explicitly provided */
|
|
41
|
+
static fromEnv(overrides?: Partial<AiAssistConfig>): AiAssistClient;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=aiassist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiassist.d.ts","sourceRoot":"","sources":["../src/aiassist.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAUD,qBAAa,aAAc,SAAQ,KAAK;aAGpB,UAAU,EAAE,MAAM;aAClB,IAAI,CAAC,EAAE,MAAM;gBAF7B,OAAO,EAAE,MAAM,EACC,UAAU,EAAE,MAAM,EAClB,IAAI,CAAC,EAAE,MAAM,YAAA;CAKhC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,MAAM,EAAE,cAAc;IAOlC,iEAAiE;IAC3D,QAAQ,CACZ,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,MAAM,CAAC;IAOlB,iCAAiC;IAC3B,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IA+C7E,kEAAkE;IAClE,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc;CAapE"}
|
package/dist/aiassist.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AiAssist.net HTTP client.
|
|
3
|
+
* Uses an OpenAI-compatible chat completions API.
|
|
4
|
+
* Set AIASSIST_API_KEY or VITE_AIAS_API_KEY in your environment.
|
|
5
|
+
*/
|
|
6
|
+
export class AiAssistError extends Error {
|
|
7
|
+
statusCode;
|
|
8
|
+
body;
|
|
9
|
+
constructor(message, statusCode, body) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.statusCode = statusCode;
|
|
12
|
+
this.body = body;
|
|
13
|
+
this.name = "AiAssistError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class AiAssistClient {
|
|
17
|
+
apiKey;
|
|
18
|
+
baseUrl;
|
|
19
|
+
model;
|
|
20
|
+
timeoutMs;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
this.apiKey = config.apiKey;
|
|
23
|
+
this.baseUrl = (config.baseUrl ?? "https://api.aiassist.net/v1").replace(/\/$/, "");
|
|
24
|
+
this.model = config.model ?? "gpt-4o";
|
|
25
|
+
this.timeoutMs = config.timeoutMs ?? 60_000;
|
|
26
|
+
}
|
|
27
|
+
/** Single-turn completion — convenience wrapper around chat() */
|
|
28
|
+
async complete(prompt, systemPrompt, options) {
|
|
29
|
+
const messages = [];
|
|
30
|
+
if (systemPrompt)
|
|
31
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
32
|
+
messages.push({ role: "user", content: prompt });
|
|
33
|
+
return this.chat(messages, options);
|
|
34
|
+
}
|
|
35
|
+
/** Multi-turn chat completion */
|
|
36
|
+
async chat(messages, options) {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
39
|
+
let response;
|
|
40
|
+
try {
|
|
41
|
+
response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
model: this.model,
|
|
49
|
+
messages,
|
|
50
|
+
temperature: options?.temperature ?? 0.3,
|
|
51
|
+
max_tokens: options?.maxTokens ?? 4_096,
|
|
52
|
+
...(options?.stop ? { stop: options.stop } : {}),
|
|
53
|
+
}),
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
if (err.name === "AbortError") {
|
|
60
|
+
throw new AiAssistError("Request timed out", 408);
|
|
61
|
+
}
|
|
62
|
+
throw new AiAssistError(`Network error: ${err.message}`, 0);
|
|
63
|
+
}
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const body = await response.text().catch(() => "");
|
|
67
|
+
throw new AiAssistError(`AiAssist API error ${response.status}`, response.status, body);
|
|
68
|
+
}
|
|
69
|
+
const data = (await response.json());
|
|
70
|
+
const content = data.choices?.[0]?.message?.content;
|
|
71
|
+
if (typeof content !== "string") {
|
|
72
|
+
throw new AiAssistError("Unexpected response shape from AiAssist API", 500);
|
|
73
|
+
}
|
|
74
|
+
return content;
|
|
75
|
+
}
|
|
76
|
+
/** Resolve API key from environment if not explicitly provided */
|
|
77
|
+
static fromEnv(overrides) {
|
|
78
|
+
const apiKey = overrides?.apiKey ??
|
|
79
|
+
process.env["AIASSIST_API_KEY"] ??
|
|
80
|
+
process.env["VITE_AIAS_API_KEY"];
|
|
81
|
+
if (!apiKey) {
|
|
82
|
+
throw new Error("AiAssist API key not found. Set AIASSIST_API_KEY or VITE_AIAS_API_KEY.");
|
|
83
|
+
}
|
|
84
|
+
return new AiAssistClient({ apiKey, ...overrides });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=aiassist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aiassist.js","sourceRoot":"","sources":["../src/aiassist.ts"],"names":[],"mappings":"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"}
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal Audit Engine — v1.1
|
|
3
|
+
*
|
|
4
|
+
* Runs structured checks across all route files against the app contract.
|
|
5
|
+
* Quality gates in the contract promote specific warns to hard fails.
|
|
6
|
+
*
|
|
7
|
+
* Categories: seo · cta · links · brand · accessibility · quality
|
|
8
|
+
*/
|
|
9
|
+
import type { AppContract } from "@interchained/portal-contract";
|
|
10
|
+
import { Runner } from "./runner.js";
|
|
11
|
+
export type CheckCategory = "seo" | "cta" | "links" | "brand" | "accessibility" | "quality";
|
|
12
|
+
export type CheckStatus = "pass" | "warn" | "fail";
|
|
13
|
+
export interface AuditFinding {
|
|
14
|
+
category: CheckCategory;
|
|
15
|
+
status: CheckStatus;
|
|
16
|
+
file: string;
|
|
17
|
+
message: string;
|
|
18
|
+
suggestion?: string;
|
|
19
|
+
line?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface AuditReport {
|
|
22
|
+
timestamp: string;
|
|
23
|
+
appName: string;
|
|
24
|
+
routesDir: string;
|
|
25
|
+
findings: AuditFinding[];
|
|
26
|
+
summary: {
|
|
27
|
+
pass: number;
|
|
28
|
+
warn: number;
|
|
29
|
+
fail: number;
|
|
30
|
+
total: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export declare function runAudit(contract: AppContract, routesDir: string, options?: {
|
|
34
|
+
ai?: boolean;
|
|
35
|
+
runner?: Runner;
|
|
36
|
+
}): Promise<AuditReport>;
|
|
37
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EACV,WAAW,EAGZ,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,MAAM,aAAa,GACrB,KAAK,GACL,KAAK,GACL,OAAO,GACP,OAAO,GACP,eAAe,GACf,SAAS,CAAC;AACd,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,aAAa,CAAC;IACxB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AA0ND,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,OAAO,CAAC,WAAW,CAAC,CA+EtB"}
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal Audit Engine — v1.1
|
|
3
|
+
*
|
|
4
|
+
* Runs structured checks across all route files against the app contract.
|
|
5
|
+
* Quality gates in the contract promote specific warns to hard fails.
|
|
6
|
+
*
|
|
7
|
+
* Categories: seo · cta · links · brand · accessibility · quality
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
10
|
+
import { join, extname, relative } from "node:path";
|
|
11
|
+
// ── Gate helpers ──────────────────────────────────────────────────────────────
|
|
12
|
+
function gated(gates, gate, defaultStatus = "warn") {
|
|
13
|
+
return gates?.[gate] === true ? "fail" : defaultStatus;
|
|
14
|
+
}
|
|
15
|
+
// ── Static checks ─────────────────────────────────────────────────────────────
|
|
16
|
+
function checkSeoStatic(content, file, page, contract) {
|
|
17
|
+
const gates = contract.qualityGates;
|
|
18
|
+
const findings = [];
|
|
19
|
+
const rel = relative(process.cwd(), file);
|
|
20
|
+
const hasHeadTitle = content.includes("<Head") ||
|
|
21
|
+
content.includes("useHead") ||
|
|
22
|
+
/title\s*[:=]/.test(content);
|
|
23
|
+
if (!hasHeadTitle) {
|
|
24
|
+
findings.push({
|
|
25
|
+
category: "seo",
|
|
26
|
+
status: gated(gates, "requireMetaTitle"),
|
|
27
|
+
file: rel,
|
|
28
|
+
message: "No <Head title> found — page is missing title metadata",
|
|
29
|
+
suggestion: `Add <Head title="..." description="..." /> from @interchained/portal-react`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const hasDescription = /description\s*[:=]/.test(content) || content.includes("meta name=\"description\"");
|
|
33
|
+
if (!hasDescription) {
|
|
34
|
+
findings.push({
|
|
35
|
+
category: "seo",
|
|
36
|
+
status: gated(gates, "requireMetaDescription"),
|
|
37
|
+
file: rel,
|
|
38
|
+
message: "No meta description found",
|
|
39
|
+
suggestion: "Add a description prop to <Head>",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (!/<h1[\s>]/i.test(content)) {
|
|
43
|
+
findings.push({
|
|
44
|
+
category: "seo",
|
|
45
|
+
status: gated(gates, "requireH1"),
|
|
46
|
+
file: rel,
|
|
47
|
+
message: "No <h1> element found — every page needs exactly one h1",
|
|
48
|
+
suggestion: "Add an <h1> with your primary heading",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const seoKeyword = page?.seoKeyword ?? contract.seo?.primaryKeyword;
|
|
52
|
+
if (seoKeyword && !content.toLowerCase().includes(seoKeyword.toLowerCase())) {
|
|
53
|
+
findings.push({
|
|
54
|
+
category: "seo",
|
|
55
|
+
status: "warn",
|
|
56
|
+
file: rel,
|
|
57
|
+
message: `Target SEO keyword "${seoKeyword}" not found in page content`,
|
|
58
|
+
suggestion: `Include "${seoKeyword}" naturally in headings or body copy`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return findings;
|
|
62
|
+
}
|
|
63
|
+
function checkCtaStatic(content, file, gates) {
|
|
64
|
+
const rel = relative(process.cwd(), file);
|
|
65
|
+
const hasCta = /<button|<Button|<Link[\s>]|href=|onClick/i.test(content);
|
|
66
|
+
if (!hasCta) {
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
category: "cta",
|
|
70
|
+
status: gated(gates, "requirePrimaryCTA"),
|
|
71
|
+
file: rel,
|
|
72
|
+
message: "No call-to-action found (button, link, or click handler)",
|
|
73
|
+
suggestion: "Every page should guide the user toward a specific action",
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
function checkAccessibilityStatic(content, file, level, gates) {
|
|
80
|
+
const findings = [];
|
|
81
|
+
const rel = relative(process.cwd(), file);
|
|
82
|
+
for (const match of content.matchAll(/<img\s[^>]*>/gi)) {
|
|
83
|
+
if (!/alt\s*=/i.test(match[0])) {
|
|
84
|
+
findings.push({
|
|
85
|
+
category: "accessibility",
|
|
86
|
+
status: gated(gates, "requireAltText", level === "strict" ? "fail" : "warn"),
|
|
87
|
+
file: rel,
|
|
88
|
+
message: "<img> tag missing alt attribute",
|
|
89
|
+
suggestion: `Add alt="" (decorative) or descriptive alt text`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (level === "strict" && !/<main[\s>]/i.test(content)) {
|
|
94
|
+
findings.push({
|
|
95
|
+
category: "accessibility",
|
|
96
|
+
status: "warn",
|
|
97
|
+
file: rel,
|
|
98
|
+
message: "No <main> landmark element found",
|
|
99
|
+
suggestion: "Wrap primary content in <main> for screen readers",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
104
|
+
function checkBrandStatic(content, file, contract) {
|
|
105
|
+
const findings = [];
|
|
106
|
+
const rel = relative(process.cwd(), file);
|
|
107
|
+
for (const phrase of contract.brand?.forbiddenPhrases ?? []) {
|
|
108
|
+
if (content.toLowerCase().includes(phrase.toLowerCase())) {
|
|
109
|
+
findings.push({
|
|
110
|
+
category: "brand",
|
|
111
|
+
status: "fail",
|
|
112
|
+
file: rel,
|
|
113
|
+
message: `Forbidden brand phrase found: "${phrase}"`,
|
|
114
|
+
suggestion: "Remove or replace — this phrase violates brand guidelines",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return findings;
|
|
119
|
+
}
|
|
120
|
+
function checkQualityStatic(content, file, gates) {
|
|
121
|
+
const findings = [];
|
|
122
|
+
const rel = relative(process.cwd(), file);
|
|
123
|
+
if (gates?.forbidPlaceholderCopy) {
|
|
124
|
+
const placeholders = ["lorem ipsum", "placeholder text", "todo:", "fixme:"];
|
|
125
|
+
for (const p of placeholders) {
|
|
126
|
+
if (content.toLowerCase().includes(p)) {
|
|
127
|
+
findings.push({
|
|
128
|
+
category: "quality",
|
|
129
|
+
status: "fail",
|
|
130
|
+
file: rel,
|
|
131
|
+
message: `Placeholder copy found: "${p}"`,
|
|
132
|
+
suggestion: "Replace with real content before shipping",
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (gates?.forbidUnreplacedTokens) {
|
|
139
|
+
const tokenMatch = content.match(/\{\{[A-Z_]+\}\}/);
|
|
140
|
+
if (tokenMatch) {
|
|
141
|
+
findings.push({
|
|
142
|
+
category: "quality",
|
|
143
|
+
status: "fail",
|
|
144
|
+
file: rel,
|
|
145
|
+
message: `Unreplaced template token found: "${tokenMatch[0]}"`,
|
|
146
|
+
suggestion: "Replace all {{TOKEN}} values with real content",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
}
|
|
152
|
+
// ── Route discovery ───────────────────────────────────────────────────────────
|
|
153
|
+
async function findRouteFiles(routesDir) {
|
|
154
|
+
const files = [];
|
|
155
|
+
try {
|
|
156
|
+
const entries = await readdir(routesDir, { recursive: true });
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
if (typeof entry === "string") {
|
|
159
|
+
const ext = extname(entry);
|
|
160
|
+
if ([".tsx", ".jsx", ".ts", ".js"].includes(ext)) {
|
|
161
|
+
files.push(join(routesDir, entry));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// routesDir doesn't exist
|
|
168
|
+
}
|
|
169
|
+
return files;
|
|
170
|
+
}
|
|
171
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
172
|
+
export async function runAudit(contract, routesDir, options = {}) {
|
|
173
|
+
const files = await findRouteFiles(routesDir);
|
|
174
|
+
const allFindings = [];
|
|
175
|
+
const accessibility = contract.policies?.accessibility ?? "basic";
|
|
176
|
+
const gates = contract.qualityGates;
|
|
177
|
+
for (const file of files) {
|
|
178
|
+
const content = await readFile(file, "utf-8");
|
|
179
|
+
const rel = relative(process.cwd(), file);
|
|
180
|
+
const routeSlug = rel
|
|
181
|
+
.replace(/^routes\//, "")
|
|
182
|
+
.replace(/\.page\.(tsx|jsx|ts|js)$/, "")
|
|
183
|
+
.replace(/index$/, "");
|
|
184
|
+
const route = "/" + routeSlug;
|
|
185
|
+
const pageContract = contract.pages?.find((p) => p.route === route);
|
|
186
|
+
allFindings.push(...checkSeoStatic(content, file, pageContract, contract));
|
|
187
|
+
allFindings.push(...checkCtaStatic(content, file, gates));
|
|
188
|
+
allFindings.push(...checkBrandStatic(content, file, contract));
|
|
189
|
+
allFindings.push(...checkQualityStatic(content, file, gates));
|
|
190
|
+
if (accessibility !== "none") {
|
|
191
|
+
allFindings.push(...checkAccessibilityStatic(content, file, accessibility, gates));
|
|
192
|
+
}
|
|
193
|
+
if (options.ai && options.runner) {
|
|
194
|
+
try {
|
|
195
|
+
const contractSummary = `Name: ${contract.name}\nGoals: ${contract.goals.join(", ")}`;
|
|
196
|
+
const raw = await options.runner.analyzeFile({
|
|
197
|
+
filePath: rel,
|
|
198
|
+
content,
|
|
199
|
+
checks: ["seo", "cta", "brand", "accessibility", "contract-alignment"],
|
|
200
|
+
contract: contractSummary,
|
|
201
|
+
});
|
|
202
|
+
const jsonStr = raw.replace(/^```json\s*/i, "").replace(/\s*```$/, "").trim();
|
|
203
|
+
const aiFindings = JSON.parse(jsonStr);
|
|
204
|
+
for (const f of aiFindings) {
|
|
205
|
+
if (f.status !== "pass") {
|
|
206
|
+
allFindings.push({
|
|
207
|
+
category: f.check,
|
|
208
|
+
status: f.status,
|
|
209
|
+
file: rel,
|
|
210
|
+
message: f.message,
|
|
211
|
+
line: f.line,
|
|
212
|
+
suggestion: f.suggestion,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// AI analysis is best-effort
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const pass = allFindings.filter((f) => f.status === "pass").length;
|
|
223
|
+
const warn = allFindings.filter((f) => f.status === "warn").length;
|
|
224
|
+
const fail = allFindings.filter((f) => f.status === "fail").length;
|
|
225
|
+
return {
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
appName: contract.name,
|
|
228
|
+
routesDir,
|
|
229
|
+
findings: allFindings,
|
|
230
|
+
summary: { pass, warn, fail, total: allFindings.length },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page / component generation.
|
|
3
|
+
*
|
|
4
|
+
* Reads the app contract, calls the Runner to generate a React component,
|
|
5
|
+
* passes it through the Sentinel for safety review, then creates a Patch
|
|
6
|
+
* for human approval (if required by policy).
|
|
7
|
+
*/
|
|
8
|
+
import type { AppContract, PageContract } from "@interchained/portal-contract";
|
|
9
|
+
import { Runner } from "./runner.js";
|
|
10
|
+
import { Sentinel } from "./sentinel.js";
|
|
11
|
+
import { type Patch } from "./patch.js";
|
|
12
|
+
export interface GenerateOptions {
|
|
13
|
+
runner: Runner;
|
|
14
|
+
sentinel?: Sentinel;
|
|
15
|
+
contract: AppContract;
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
/** Override the policy — useful for interactive approve-later flow */
|
|
18
|
+
requiresApproval?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface GenerateResult {
|
|
21
|
+
patch: Patch;
|
|
22
|
+
sentinelReview?: {
|
|
23
|
+
approved: boolean;
|
|
24
|
+
summary: string;
|
|
25
|
+
violations: string[];
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate a new page component from a PageContract or an ad-hoc prompt.
|
|
30
|
+
*/
|
|
31
|
+
export declare function generatePage(page: PageContract & {
|
|
32
|
+
description?: string;
|
|
33
|
+
}, opts: GenerateOptions): Promise<GenerateResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Generate a page from a freeform description (no pre-existing PageContract).
|
|
36
|
+
* `portal generate page "Father's Day promo"` style.
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateFromPrompt(prompt: string, opts: GenerateOptions & {
|
|
39
|
+
route?: string;
|
|
40
|
+
}): Promise<GenerateResult>;
|
|
41
|
+
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAe,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,EAAE,WAAW,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,CAAC,EAAE;QACf,QAAQ,EAAE,OAAO,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,EAAE,CAAC;KACtB,CAAC;CACH;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,YAAY,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7C,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,cAAc,CAAC,CAwDzB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,eAAe,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACzC,OAAO,CAAC,cAAc,CAAC,CASzB"}
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page / component generation.
|
|
3
|
+
*
|
|
4
|
+
* Reads the app contract, calls the Runner to generate a React component,
|
|
5
|
+
* passes it through the Sentinel for safety review, then creates a Patch
|
|
6
|
+
* for human approval (if required by policy).
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { createPatch } from "./patch.js";
|
|
11
|
+
/**
|
|
12
|
+
* Generate a new page component from a PageContract or an ad-hoc prompt.
|
|
13
|
+
*/
|
|
14
|
+
export async function generatePage(page, opts) {
|
|
15
|
+
const { runner, sentinel, contract } = opts;
|
|
16
|
+
// Check if the file already exists — use as context
|
|
17
|
+
const fileName = routeToFileName(page.route);
|
|
18
|
+
const filePath = join(opts.projectRoot, "routes", fileName);
|
|
19
|
+
let original = "";
|
|
20
|
+
try {
|
|
21
|
+
original = await readFile(filePath, "utf-8");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// New file — original is empty
|
|
25
|
+
}
|
|
26
|
+
const proposed = await runner.generateComponent({
|
|
27
|
+
route: page.route,
|
|
28
|
+
purpose: page.purpose,
|
|
29
|
+
audience: page.audience,
|
|
30
|
+
primaryAction: page.primaryAction,
|
|
31
|
+
brandVoice: contract.brand?.voice,
|
|
32
|
+
colors: contract.brand?.colors,
|
|
33
|
+
});
|
|
34
|
+
const requiresApproval = opts.requiresApproval ??
|
|
35
|
+
contract.policies?.publishing === "human_review";
|
|
36
|
+
let sentinelResult;
|
|
37
|
+
if (sentinel) {
|
|
38
|
+
const review = await sentinel.review({
|
|
39
|
+
contract,
|
|
40
|
+
filePath,
|
|
41
|
+
original,
|
|
42
|
+
proposed,
|
|
43
|
+
agentTask: `Generate page for route "${page.route}": ${page.purpose}`,
|
|
44
|
+
});
|
|
45
|
+
sentinelResult = {
|
|
46
|
+
approved: review.approved,
|
|
47
|
+
summary: review.summary,
|
|
48
|
+
violations: review.violations,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const patch = createPatch({
|
|
52
|
+
agent: "portal-generate",
|
|
53
|
+
file: join("routes", fileName),
|
|
54
|
+
original,
|
|
55
|
+
proposed,
|
|
56
|
+
reason: `Generate page: ${page.purpose}`,
|
|
57
|
+
requiresApproval,
|
|
58
|
+
sentinelApproved: sentinelResult?.approved,
|
|
59
|
+
sentinelSummary: sentinelResult?.summary,
|
|
60
|
+
sentinelViolations: sentinelResult?.violations,
|
|
61
|
+
});
|
|
62
|
+
return { patch, sentinelReview: sentinelResult };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generate a page from a freeform description (no pre-existing PageContract).
|
|
66
|
+
* `portal generate page "Father's Day promo"` style.
|
|
67
|
+
*/
|
|
68
|
+
export async function generateFromPrompt(prompt, opts) {
|
|
69
|
+
const route = opts.route ?? promptToRoute(prompt);
|
|
70
|
+
const page = {
|
|
71
|
+
route,
|
|
72
|
+
purpose: prompt,
|
|
73
|
+
audience: undefined,
|
|
74
|
+
primaryAction: undefined,
|
|
75
|
+
};
|
|
76
|
+
return generatePage(page, opts);
|
|
77
|
+
}
|
|
78
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
79
|
+
function routeToFileName(route) {
|
|
80
|
+
const clean = route.replace(/^\//, "") || "index";
|
|
81
|
+
return `${clean}.page.tsx`;
|
|
82
|
+
}
|
|
83
|
+
function promptToRoute(prompt) {
|
|
84
|
+
return ("/" +
|
|
85
|
+
prompt
|
|
86
|
+
.toLowerCase()
|
|
87
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
88
|
+
.trim()
|
|
89
|
+
.replace(/\s+/g, "-")
|
|
90
|
+
.slice(0, 48));
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=generate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.js","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"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"}
|
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal Guard — prevents bad AI output from entering the codebase.
|
|
3
|
+
*
|
|
4
|
+
* Runs a set of deterministic safety checks BEFORE any patch is applied.
|
|
5
|
+
* Unlike audit (which checks existing code), guard checks proposed patches.
|
|
6
|
+
*
|
|
7
|
+
* Checks:
|
|
8
|
+
* - No hallucinated phone numbers or emails (compares against data sources)
|
|
9
|
+
* - No changed brand colors without approval
|
|
10
|
+
* - No forbidden claims
|
|
11
|
+
* - No broken internal links (routes that don't exist)
|
|
12
|
+
* - No unsafe dependency additions
|
|
13
|
+
*/
|
|
14
|
+
import type { AppContract } from "@interchained/portal-contract";
|
|
15
|
+
import type { Patch } from "./patch.js";
|
|
16
|
+
export interface GuardViolation {
|
|
17
|
+
rule: string;
|
|
18
|
+
severity: "block" | "warn";
|
|
19
|
+
message: string;
|
|
20
|
+
evidence?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface GuardResult {
|
|
23
|
+
patchId: string;
|
|
24
|
+
passed: boolean;
|
|
25
|
+
violations: GuardViolation[];
|
|
26
|
+
}
|
|
27
|
+
export declare function guardPatch(patch: Patch, contract: AppContract): Promise<GuardResult>;
|
|
28
|
+
/** Run guard checks across all pending patches */
|
|
29
|
+
export declare function guardAllPending(patches: Patch[], contract: AppContract): Promise<GuardResult[]>;
|
|
30
|
+
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAmGD,wBAAsB,UAAU,CAC9B,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,WAAW,CAAC,CAetB;AAED,kDAAkD;AAClD,wBAAsB,eAAe,CACnC,OAAO,EAAE,KAAK,EAAE,EAChB,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAGxB"}
|