@amityco/social-plus-vise 0.4.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/LICENSE +51 -0
- package/README.md +92 -0
- package/dist/outcomes.js +574 -0
- package/dist/server.js +810 -0
- package/dist/tools/compliance.js +965 -0
- package/dist/tools/docs.js +312 -0
- package/dist/tools/harness.js +229 -0
- package/dist/tools/integration.js +332 -0
- package/dist/tools/patch.js +67 -0
- package/dist/tools/project.js +908 -0
- package/dist/tools/resolve.js +120 -0
- package/dist/tools/sensors.js +185 -0
- package/dist/types.js +31 -0
- package/dist/version.js +19 -0
- package/package.json +64 -0
- package/rules/design.yaml +66 -0
- package/rules/feed.yaml +126 -0
- package/rules/live-data.yaml +66 -0
- package/rules/push.yaml +95 -0
- package/rules/sdk-lifecycle.yaml +422 -0
- package/rules/security.yaml +162 -0
- package/skills/social-plus-vise/SKILL.md +199 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { classifyOutcome, getOutcomeDefinition } from "../outcomes.js";
|
|
2
|
+
import { objectInput, optionalStringField, stringField, textResult } from "../types.js";
|
|
3
|
+
import { harnessControlsFor } from "./harness.js";
|
|
4
|
+
import { inspectProject } from "./project.js";
|
|
5
|
+
export const resolveRequestTool = {
|
|
6
|
+
name: "resolve_request",
|
|
7
|
+
description: "Resolve a natural-language social.plus integration request into a support level and next tools.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
repoPath: { type: "string" },
|
|
12
|
+
request: { type: "string" },
|
|
13
|
+
surfacePath: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Optional app/workspace path inside repoPath, such as apps/web or apps/mobile.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ["repoPath", "request"],
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
},
|
|
21
|
+
async call(input) {
|
|
22
|
+
const args = objectInput(input);
|
|
23
|
+
const repoPath = stringField(args, "repoPath");
|
|
24
|
+
const request = stringField(args, "request");
|
|
25
|
+
const inspection = await inspectProject(repoPath, optionalStringField(args, "surfacePath"));
|
|
26
|
+
const outcome = classifyOutcome(request);
|
|
27
|
+
const supportLevel = supportFor(outcome, inspection.platforms);
|
|
28
|
+
return textResult({
|
|
29
|
+
outcome,
|
|
30
|
+
supportLevel,
|
|
31
|
+
surface: inspection.selectedSurface ? { path: inspection.selectedSurface.path, platforms: inspection.selectedSurface.platforms } : undefined,
|
|
32
|
+
availableSurfaces: inspection.surfaces.map((surface) => ({ path: surface.path, platforms: surface.platforms })),
|
|
33
|
+
targetPlatforms: inspection.platforms,
|
|
34
|
+
nextTools: nextToolsFor(outcome, supportLevel),
|
|
35
|
+
harness: harnessControlsFor(outcome, inspection.platforms),
|
|
36
|
+
notes: notesFor(outcome, supportLevel, inspection.platforms),
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export const suggestPatchTool = {
|
|
41
|
+
name: "suggest_patch",
|
|
42
|
+
description: "Deprecated compatibility tool. Prefer plan_integration for grounded implementation planning. This tool does not write files.",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
repoPath: { type: "string" },
|
|
47
|
+
request: { type: "string" },
|
|
48
|
+
},
|
|
49
|
+
required: ["repoPath", "request"],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
async call(input) {
|
|
53
|
+
const args = objectInput(input);
|
|
54
|
+
const repoPath = stringField(args, "repoPath");
|
|
55
|
+
const request = stringField(args, "request");
|
|
56
|
+
const inspection = await inspectProject(repoPath);
|
|
57
|
+
const outcome = classifyOutcome(request);
|
|
58
|
+
return textResult({
|
|
59
|
+
deprecated: true,
|
|
60
|
+
replacement: "plan_integration",
|
|
61
|
+
writes: [],
|
|
62
|
+
outcome,
|
|
63
|
+
targetPlatforms: inspection.platforms,
|
|
64
|
+
plan: planFor(outcome, inspection.platforms),
|
|
65
|
+
harness: harnessControlsFor(outcome, inspection.platforms),
|
|
66
|
+
verification: verificationFor(outcome, inspection.platforms),
|
|
67
|
+
safety: "Read-only v1: no files were modified.",
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
function supportFor(outcome, platforms) {
|
|
72
|
+
if (outcome === "unknown") {
|
|
73
|
+
return "unsupported";
|
|
74
|
+
}
|
|
75
|
+
if (platforms.length === 0) {
|
|
76
|
+
return "guided";
|
|
77
|
+
}
|
|
78
|
+
if (platforms.some((platform) => ["android", "flutter", "typescript", "react-native"].includes(platform))) {
|
|
79
|
+
return "supported";
|
|
80
|
+
}
|
|
81
|
+
if (platforms.includes("ios")) {
|
|
82
|
+
return "guided";
|
|
83
|
+
}
|
|
84
|
+
return "guided";
|
|
85
|
+
}
|
|
86
|
+
function nextToolsFor(outcome, supportLevel) {
|
|
87
|
+
if (supportLevel === "unsupported") {
|
|
88
|
+
return ["search_docs"];
|
|
89
|
+
}
|
|
90
|
+
if (outcome === "troubleshoot") {
|
|
91
|
+
return ["inspect_project", "search_docs", "get_doc_page", "validate_setup"];
|
|
92
|
+
}
|
|
93
|
+
return ["plan_harness", "plan_integration", "inspect_project", "search_docs", "get_doc_page", "validate_setup", "run_sensors"];
|
|
94
|
+
}
|
|
95
|
+
function notesFor(outcome, supportLevel, platforms) {
|
|
96
|
+
const notes = [];
|
|
97
|
+
if (platforms.length === 0) {
|
|
98
|
+
notes.push("No platform was detected. Ask for the app framework or point repoPath at the project root.");
|
|
99
|
+
}
|
|
100
|
+
if (platforms.includes("ios")) {
|
|
101
|
+
notes.push("iOS should be treated as guided until setup validation rules are expanded.");
|
|
102
|
+
}
|
|
103
|
+
if (supportLevel === "unsupported") {
|
|
104
|
+
notes.push("The request does not map to a known Vise outcome.");
|
|
105
|
+
}
|
|
106
|
+
notes.push(...getOutcomeDefinition(outcome).resolveNotes(platforms));
|
|
107
|
+
return notes;
|
|
108
|
+
}
|
|
109
|
+
function planFor(outcome, platforms) {
|
|
110
|
+
const platform = platforms[0] ?? "unknown";
|
|
111
|
+
return getOutcomeDefinition(outcome).resolvePlan(platform);
|
|
112
|
+
}
|
|
113
|
+
function verificationFor(outcome, platforms) {
|
|
114
|
+
const checks = ["Run the app's normal build or typecheck command."];
|
|
115
|
+
checks.push(...getOutcomeDefinition(outcome).resolveVerification(platforms[0] ?? "unknown"));
|
|
116
|
+
if (platforms.includes("android")) {
|
|
117
|
+
checks.push("For Android, confirm INTERNET permission exists in AndroidManifest.xml.");
|
|
118
|
+
}
|
|
119
|
+
return checks;
|
|
120
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { objectInput, optionalNumberField, optionalStringField, stringField, textResult } from "../types.js";
|
|
4
|
+
import { detectCommandSensors } from "./harness.js";
|
|
5
|
+
import { inspectProject } from "./project.js";
|
|
6
|
+
export const runSensorsTool = {
|
|
7
|
+
name: "run_sensors",
|
|
8
|
+
description: "Run detected project command sensors, such as build/typecheck/test, with a timeout and structured results.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
repoPath: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Absolute or relative path to the customer repository root.",
|
|
15
|
+
},
|
|
16
|
+
request: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Natural-language integration request. Used for traceability; command sensors are detected from the project.",
|
|
19
|
+
},
|
|
20
|
+
include: {
|
|
21
|
+
type: "array",
|
|
22
|
+
items: { type: "string" },
|
|
23
|
+
description: "Optional list of sensor names to run. If omitted, all detected command sensors run.",
|
|
24
|
+
},
|
|
25
|
+
timeoutMs: {
|
|
26
|
+
type: "number",
|
|
27
|
+
default: 120000,
|
|
28
|
+
description: "Per-command timeout in milliseconds.",
|
|
29
|
+
},
|
|
30
|
+
dryRun: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
default: false,
|
|
33
|
+
description: "If true, return detected sensors without executing commands.",
|
|
34
|
+
},
|
|
35
|
+
surfacePath: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Optional app/workspace path inside repoPath, such as apps/web or apps/mobile.",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ["repoPath"],
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
},
|
|
43
|
+
async call(input) {
|
|
44
|
+
const args = objectInput(input);
|
|
45
|
+
const repoPath = stringField(args, "repoPath");
|
|
46
|
+
const timeoutMs = Math.max(1000, Math.min(optionalNumberField(args, "timeoutMs", 120000), 600000));
|
|
47
|
+
const dryRun = args.dryRun === true;
|
|
48
|
+
const include = stringArrayField(args, "include");
|
|
49
|
+
const root = path.resolve(repoPath);
|
|
50
|
+
const inspection = await inspectProject(root, optionalStringField(args, "surfacePath"));
|
|
51
|
+
const detectedSensors = await detectCommandSensors(inspection.effectiveRoot, inspection.platforms);
|
|
52
|
+
const selectedSensors = include.length > 0 ? detectedSensors.filter((sensor) => include.includes(sensor.name)) : detectedSensors;
|
|
53
|
+
if (dryRun) {
|
|
54
|
+
return textResult({
|
|
55
|
+
status: "dry-run",
|
|
56
|
+
surfacePath: inspection.selectedSurface?.path,
|
|
57
|
+
targetPlatforms: inspection.platforms,
|
|
58
|
+
sensors: selectedSensors,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const results = [];
|
|
62
|
+
for (const sensor of selectedSensors) {
|
|
63
|
+
results.push(await runSensor(inspection.effectiveRoot, sensor, timeoutMs));
|
|
64
|
+
}
|
|
65
|
+
return textResult({
|
|
66
|
+
status: aggregateStatus(results),
|
|
67
|
+
surfacePath: inspection.selectedSurface?.path,
|
|
68
|
+
targetPlatforms: inspection.platforms,
|
|
69
|
+
results,
|
|
70
|
+
skipped: skippedSensors(detectedSensors, selectedSensors, include),
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
async function runSensor(cwd, sensor, timeoutMs) {
|
|
75
|
+
const startedAt = Date.now();
|
|
76
|
+
const [command, ...args] = sensor.command;
|
|
77
|
+
if (!command) {
|
|
78
|
+
return {
|
|
79
|
+
name: sensor.name,
|
|
80
|
+
command: sensor.command,
|
|
81
|
+
status: "skipped",
|
|
82
|
+
reason: "Sensor command is empty.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
let stdout = "";
|
|
87
|
+
let stderr = "";
|
|
88
|
+
let settled = false;
|
|
89
|
+
const child = spawn(command, args, {
|
|
90
|
+
cwd,
|
|
91
|
+
shell: false,
|
|
92
|
+
env: process.env,
|
|
93
|
+
});
|
|
94
|
+
const timeout = setTimeout(() => {
|
|
95
|
+
if (settled) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
settled = true;
|
|
99
|
+
child.kill("SIGTERM");
|
|
100
|
+
resolve({
|
|
101
|
+
name: sensor.name,
|
|
102
|
+
command: sensor.command,
|
|
103
|
+
status: "timed-out",
|
|
104
|
+
durationMs: Date.now() - startedAt,
|
|
105
|
+
stdout: truncate(stdout),
|
|
106
|
+
stderr: truncate(stderr),
|
|
107
|
+
reason: `Timed out after ${timeoutMs}ms.`,
|
|
108
|
+
});
|
|
109
|
+
}, timeoutMs);
|
|
110
|
+
child.stdout?.on("data", (chunk) => {
|
|
111
|
+
stdout += chunk.toString("utf8");
|
|
112
|
+
});
|
|
113
|
+
child.stderr?.on("data", (chunk) => {
|
|
114
|
+
stderr += chunk.toString("utf8");
|
|
115
|
+
});
|
|
116
|
+
child.on("error", (error) => {
|
|
117
|
+
if (settled) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
settled = true;
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
resolve({
|
|
123
|
+
name: sensor.name,
|
|
124
|
+
command: sensor.command,
|
|
125
|
+
status: "failed",
|
|
126
|
+
durationMs: Date.now() - startedAt,
|
|
127
|
+
stderr: truncate(error.message),
|
|
128
|
+
reason: "Failed to start sensor command.",
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
child.on("close", (exitCode) => {
|
|
132
|
+
if (settled) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
settled = true;
|
|
136
|
+
clearTimeout(timeout);
|
|
137
|
+
resolve({
|
|
138
|
+
name: sensor.name,
|
|
139
|
+
command: sensor.command,
|
|
140
|
+
status: exitCode === 0 ? "passed" : "failed",
|
|
141
|
+
exitCode,
|
|
142
|
+
durationMs: Date.now() - startedAt,
|
|
143
|
+
stdout: truncate(stdout),
|
|
144
|
+
stderr: truncate(stderr),
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function aggregateStatus(results) {
|
|
150
|
+
if (results.length === 0) {
|
|
151
|
+
return "no-sensors";
|
|
152
|
+
}
|
|
153
|
+
if (results.some((result) => result.status === "timed-out")) {
|
|
154
|
+
return "timed-out";
|
|
155
|
+
}
|
|
156
|
+
if (results.some((result) => result.status === "failed")) {
|
|
157
|
+
return "failed";
|
|
158
|
+
}
|
|
159
|
+
return "passed";
|
|
160
|
+
}
|
|
161
|
+
function skippedSensors(detectedSensors, selectedSensors, include) {
|
|
162
|
+
const selectedNames = new Set(selectedSensors.map((sensor) => sensor.name));
|
|
163
|
+
const skipped = detectedSensors
|
|
164
|
+
.filter((sensor) => !selectedNames.has(sensor.name))
|
|
165
|
+
.map((sensor) => ({ name: sensor.name, reason: "Not selected by include filter." }));
|
|
166
|
+
for (const requestedName of include) {
|
|
167
|
+
if (!detectedSensors.some((sensor) => sensor.name === requestedName)) {
|
|
168
|
+
skipped.push({ name: requestedName, reason: "No detected sensor matched this include name." });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return skipped;
|
|
172
|
+
}
|
|
173
|
+
function stringArrayField(input, field) {
|
|
174
|
+
const value = input[field];
|
|
175
|
+
if (!Array.isArray(value)) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
return value.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
179
|
+
}
|
|
180
|
+
function truncate(value, maxLength = 8000) {
|
|
181
|
+
if (value.length <= maxLength) {
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
return `${value.slice(0, maxLength)}\n[truncated ${value.length - maxLength} chars]`;
|
|
185
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function textResult(value) {
|
|
2
|
+
return {
|
|
3
|
+
content: [
|
|
4
|
+
{
|
|
5
|
+
type: "text",
|
|
6
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
|
|
7
|
+
},
|
|
8
|
+
],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function objectInput(input) {
|
|
12
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
return input;
|
|
16
|
+
}
|
|
17
|
+
export function stringField(input, field) {
|
|
18
|
+
const value = input[field];
|
|
19
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
20
|
+
throw new Error(`Missing required string field: ${field}`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
export function optionalStringField(input, field) {
|
|
25
|
+
const value = input[field];
|
|
26
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
export function optionalNumberField(input, field, fallback) {
|
|
29
|
+
const value = input[field];
|
|
30
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
31
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
function readPackageMetadata() {
|
|
5
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const packageJsonPath = path.resolve(moduleDir, "..", "package.json");
|
|
7
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
8
|
+
if (typeof parsed.name !== "string" || typeof parsed.version !== "string") {
|
|
9
|
+
throw new Error(`Invalid package metadata in ${packageJsonPath}.`);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
name: parsed.name,
|
|
13
|
+
version: parsed.version,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const packageMetadata = readPackageMetadata();
|
|
17
|
+
export const packageName = packageMetadata.name;
|
|
18
|
+
export const packageVersion = packageMetadata.version;
|
|
19
|
+
export const packageUserAgent = `social-plus-vise/${packageVersion}`;
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@amityco/social-plus-vise",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Skill-guided deterministic CLI for social.plus SDK integration assistance.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/AmityCo/social-plus-foundry.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/AmityCo/social-plus-foundry#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/AmityCo/social-plus-foundry/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"social-plus",
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"sdk",
|
|
20
|
+
"ai-coding"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"vise": "dist/server.js",
|
|
30
|
+
"spf": "dist/server.js",
|
|
31
|
+
"social-plus-vise": "dist/server.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"README.md",
|
|
37
|
+
"rules",
|
|
38
|
+
"skills"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"postbuild": "chmod +x dist/server.js",
|
|
43
|
+
"pack:check": "npm pack --dry-run --cache /private/tmp/social-plus-foundry-npm-cache",
|
|
44
|
+
"publish:check": "npm run validate && npm publish --dry-run --access public --cache /private/tmp/social-plus-foundry-npm-cache",
|
|
45
|
+
"start": "node dist/server.js",
|
|
46
|
+
"test": "npm run build && node test/run-fixtures.mjs",
|
|
47
|
+
"test:cli": "npm run build && node test/run-cli.mjs",
|
|
48
|
+
"test:compliance": "npm run build && node test/run-compliance-helpers.mjs",
|
|
49
|
+
"test:docs": "npm run build && node test/run-docs-parser.mjs",
|
|
50
|
+
"test:improvements": "npm run build && node test/run-improvements.mjs",
|
|
51
|
+
"test:mcp": "npm run build && node test/run-mcp-smoke.mjs",
|
|
52
|
+
"test:readme-coverage": "node test/run-readme-coverage.mjs",
|
|
53
|
+
"test:rule-coverage": "npm run build && node test/run-rule-coverage.mjs",
|
|
54
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
|
+
"validate": "npm run typecheck && npm test && npm run test:mcp && npm run test:cli && npm run test:docs && npm run test:compliance && npm run test:rule-coverage && npm run test:readme-coverage && npm run test:improvements && npm run pack:check"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^20.11.30",
|
|
62
|
+
"typescript": "^5.4.5"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"domain": "design",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"rules": [
|
|
5
|
+
{
|
|
6
|
+
"id": "android.design.reuse-detected-tokens",
|
|
7
|
+
"version": 1,
|
|
8
|
+
"title": "Android UI code must reuse the detected design tokens",
|
|
9
|
+
"severity": "warning",
|
|
10
|
+
"rationale": "When the project ships theme/token/Compose color files, social UI built without referencing them produces inline-styled surfaces that drift from the host app's brand. Customers report this as 'the agent ignored our design system'.",
|
|
11
|
+
"applies_when": { "platforms": ["android"], "outcomes": ["add-feed", "validate-setup"] },
|
|
12
|
+
"enforcement": {
|
|
13
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.design.reuse-detected-tokens" }],
|
|
14
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "design_usage", "description": "Where the new UI imports or references the detected design tokens (file path + symbol).", "upload_policy": "upload-with-consent" }] }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "flutter.design.reuse-detected-tokens",
|
|
19
|
+
"version": 1,
|
|
20
|
+
"title": "Flutter UI code must reuse the detected design tokens",
|
|
21
|
+
"severity": "warning",
|
|
22
|
+
"rationale": "When the project ships theme.dart / app_theme.dart, new Flutter widgets should pull colors and typography from the existing ThemeData rather than hardcoded Color() literals.",
|
|
23
|
+
"applies_when": { "platforms": ["flutter"], "outcomes": ["add-feed", "validate-setup"] },
|
|
24
|
+
"enforcement": {
|
|
25
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.design.reuse-detected-tokens" }],
|
|
26
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "design_usage", "description": "Where the new widget imports or applies the detected ThemeData.", "upload_policy": "upload-with-consent" }] }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "ios.design.reuse-detected-tokens",
|
|
31
|
+
"version": 1,
|
|
32
|
+
"title": "iOS UI code must reuse the detected design tokens",
|
|
33
|
+
"severity": "warning",
|
|
34
|
+
"rationale": "When the project ships AppTheme.swift / Tokens.swift / xcassets, new Swift UI surfaces should reference them instead of inline UIColor / Color values.",
|
|
35
|
+
"applies_when": { "platforms": ["ios"], "outcomes": ["add-feed", "validate-setup"] },
|
|
36
|
+
"enforcement": {
|
|
37
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.design.reuse-detected-tokens" }],
|
|
38
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "design_usage", "description": "Where the new view references the detected AppTheme or asset catalog tokens.", "upload_policy": "upload-with-consent" }] }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "typescript.design.reuse-detected-tokens",
|
|
43
|
+
"version": 1,
|
|
44
|
+
"title": "TypeScript UI code must reuse the detected design tokens",
|
|
45
|
+
"severity": "warning",
|
|
46
|
+
"rationale": "When the project ships theme modules, Tailwind config, shadcn UI components, or design-token files, new social UI should import them instead of inline style props.",
|
|
47
|
+
"applies_when": { "platforms": ["typescript"], "outcomes": ["add-feed", "validate-setup"] },
|
|
48
|
+
"enforcement": {
|
|
49
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.design.reuse-detected-tokens" }],
|
|
50
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "design_usage", "description": "Where the new component imports the detected design tokens / UI kit (e.g., Tailwind config, shadcn component, theme module).", "upload_policy": "upload-with-consent" }] }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "react-native.design.reuse-detected-tokens",
|
|
55
|
+
"version": 1,
|
|
56
|
+
"title": "React Native UI code must reuse the detected design tokens",
|
|
57
|
+
"severity": "warning",
|
|
58
|
+
"rationale": "When the project ships theme modules or a design-system file, new social UI should reference them instead of inline StyleSheet color/spacing literals.",
|
|
59
|
+
"applies_when": { "platforms": ["react-native"], "outcomes": ["add-feed", "validate-setup"] },
|
|
60
|
+
"enforcement": {
|
|
61
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "react-native.design.reuse-detected-tokens" }],
|
|
62
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "design_usage", "description": "Where the new component imports the detected design tokens / theme module.", "upload_policy": "upload-with-consent" }] }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
package/rules/feed.yaml
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"domain": "feed",
|
|
3
|
+
"schema_version": 1,
|
|
4
|
+
"rules": [
|
|
5
|
+
{
|
|
6
|
+
"id": "typescript.feed.target.literal",
|
|
7
|
+
"version": 1,
|
|
8
|
+
"title": "TypeScript feed targets must not be hardcoded literals",
|
|
9
|
+
"severity": "warning",
|
|
10
|
+
"rationale": "Feed targets such as communityId, targetId, feedId, and channelId must come from customer app state, not invented source literals.",
|
|
11
|
+
"applies_when": { "platforms": ["typescript"], "outcomes": ["add-feed", "validate-setup"] },
|
|
12
|
+
"enforcement": {
|
|
13
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.feed.target.literal" }],
|
|
14
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "target_source", "description": "Where the runtime feed target comes from.", "upload_policy": "upload-with-consent" }] }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "react-native.feed.target.literal",
|
|
19
|
+
"version": 1,
|
|
20
|
+
"title": "React Native feed targets must not be hardcoded literals",
|
|
21
|
+
"severity": "warning",
|
|
22
|
+
"rationale": "Feed targets such as communityId, targetId, feedId, and channelId must come from customer app state, not invented source literals.",
|
|
23
|
+
"applies_when": { "platforms": ["react-native"], "outcomes": ["add-feed", "validate-setup"] },
|
|
24
|
+
"enforcement": {
|
|
25
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "react-native.feed.target.literal" }],
|
|
26
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "target_source", "description": "Where the runtime feed target comes from.", "upload_policy": "upload-with-consent" }] }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "android.feed.target.literal",
|
|
31
|
+
"version": 1,
|
|
32
|
+
"title": "Android feed targets must not be hardcoded literals",
|
|
33
|
+
"severity": "warning",
|
|
34
|
+
"rationale": "Feed targets such as communityId, targetId, feedId, and channelId must come from customer app state, not invented source literals.",
|
|
35
|
+
"applies_when": { "platforms": ["android"], "outcomes": ["add-feed", "validate-setup"] },
|
|
36
|
+
"enforcement": {
|
|
37
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.feed.target.literal" }],
|
|
38
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "target_source", "description": "Where the runtime feed target comes from.", "upload_policy": "upload-with-consent" }] }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "flutter.feed.target.literal",
|
|
43
|
+
"version": 1,
|
|
44
|
+
"title": "Flutter feed targets must not be hardcoded literals",
|
|
45
|
+
"severity": "warning",
|
|
46
|
+
"rationale": "Feed targets such as communityId, targetId, feedId, and channelId must come from customer app state, not invented source literals.",
|
|
47
|
+
"applies_when": { "platforms": ["flutter"], "outcomes": ["add-feed", "validate-setup"] },
|
|
48
|
+
"enforcement": {
|
|
49
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.feed.target.literal" }],
|
|
50
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "target_source", "description": "Where the runtime feed target comes from.", "upload_policy": "upload-with-consent" }] }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "ios.feed.target.literal",
|
|
55
|
+
"version": 1,
|
|
56
|
+
"title": "iOS feed targets must not be hardcoded literals",
|
|
57
|
+
"severity": "warning",
|
|
58
|
+
"rationale": "Feed targets such as communityId, targetId, feedId, and channelId must come from customer app state, not invented source literals.",
|
|
59
|
+
"applies_when": { "platforms": ["ios"], "outcomes": ["add-feed", "validate-setup"] },
|
|
60
|
+
"enforcement": {
|
|
61
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.feed.target.literal" }],
|
|
62
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "target_source", "description": "Where the runtime feed target comes from.", "upload_policy": "upload-with-consent" }] }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "android.feed.ui-states-present",
|
|
67
|
+
"version": 1,
|
|
68
|
+
"title": "Android feed UI must render loading, empty, and error states",
|
|
69
|
+
"severity": "warning",
|
|
70
|
+
"rationale": "Live collections take time to load, can be empty, and can error. A feed that only renders the success path looks broken to users on slow networks or fresh accounts.",
|
|
71
|
+
"applies_when": { "platforms": ["android"], "outcomes": ["add-feed", "setup-live-data", "validate-setup"] },
|
|
72
|
+
"enforcement": {
|
|
73
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "android.feed.ui-states-present" }],
|
|
74
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "state_handling", "description": "Where loading, empty, and error states are rendered (or why they aren't needed).", "upload_policy": "upload-with-consent" }] }
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "flutter.feed.ui-states-present",
|
|
79
|
+
"version": 1,
|
|
80
|
+
"title": "Flutter feed UI must render loading, empty, and error states",
|
|
81
|
+
"severity": "warning",
|
|
82
|
+
"rationale": "Live streams take time to load, can be empty, and can error. A feed widget that only renders posts looks broken on slow networks or fresh accounts.",
|
|
83
|
+
"applies_when": { "platforms": ["flutter"], "outcomes": ["add-feed", "setup-live-data", "validate-setup"] },
|
|
84
|
+
"enforcement": {
|
|
85
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "flutter.feed.ui-states-present" }],
|
|
86
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "state_handling", "description": "Where loading (CircularProgressIndicator), empty, and error states are rendered.", "upload_policy": "upload-with-consent" }] }
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "ios.feed.ui-states-present",
|
|
91
|
+
"version": 1,
|
|
92
|
+
"title": "iOS feed UI must render loading, empty, and error states",
|
|
93
|
+
"severity": "warning",
|
|
94
|
+
"rationale": "Live collections take time to load, can be empty, and can error. A feed view that only renders the success path looks broken on slow networks or fresh accounts.",
|
|
95
|
+
"applies_when": { "platforms": ["ios"], "outcomes": ["add-feed", "setup-live-data", "validate-setup"] },
|
|
96
|
+
"enforcement": {
|
|
97
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "ios.feed.ui-states-present" }],
|
|
98
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "state_handling", "description": "Where loading (ProgressView), empty, and error states are rendered.", "upload_policy": "upload-with-consent" }] }
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "typescript.feed.ui-states-present",
|
|
103
|
+
"version": 1,
|
|
104
|
+
"title": "TypeScript feed UI must render loading, empty, and error states",
|
|
105
|
+
"severity": "warning",
|
|
106
|
+
"rationale": "Live subscriptions take time to load, can be empty, and can error. A component that only renders posts looks broken on slow networks or fresh accounts.",
|
|
107
|
+
"applies_when": { "platforms": ["typescript"], "outcomes": ["add-feed", "setup-live-data", "validate-setup"] },
|
|
108
|
+
"enforcement": {
|
|
109
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "typescript.feed.ui-states-present" }],
|
|
110
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "state_handling", "description": "Where loading, empty, and error states are rendered (e.g., isLoading state, empty fallback, error boundary).", "upload_policy": "upload-with-consent" }] }
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"id": "react-native.feed.ui-states-present",
|
|
115
|
+
"version": 1,
|
|
116
|
+
"title": "React Native feed UI must render loading, empty, and error states",
|
|
117
|
+
"severity": "warning",
|
|
118
|
+
"rationale": "Live subscriptions take time to load, can be empty, and can error. A component that only renders posts looks broken on slow networks or fresh accounts.",
|
|
119
|
+
"applies_when": { "platforms": ["react-native"], "outcomes": ["add-feed", "setup-live-data", "validate-setup"] },
|
|
120
|
+
"enforcement": {
|
|
121
|
+
"deterministic": [{ "check": "validator-finding-absent", "finding_rule_id": "react-native.feed.ui-states-present" }],
|
|
122
|
+
"attestation": { "allowed": true, "host_agent_min_confidence": "high", "human_allowed": true, "evidence_required": [{ "field": "state_handling", "description": "Where loading (ActivityIndicator), empty, and error states are rendered.", "upload_policy": "upload-with-consent" }] }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|