@cassiomc1/forgeloop 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/.cursor/rules/project-loop.mdc +18 -0
- package/.forgeloop/.gitignore +2 -0
- package/.github/copilot-instructions.md +16 -0
- package/AGENTS.md +16 -0
- package/AGENT_COMPATIBILITY.md +147 -0
- package/CLAUDE.md +14 -0
- package/CONTRACT_COVERAGE.md +27 -0
- package/DELEGATION_PROTOCOL.md +91 -0
- package/ENG/accessibility-eng.md +155 -0
- package/ENG/clean-code-eng.md +223 -0
- package/ENG/design-code-eng.md +511 -0
- package/ENG/games-code-design-web-eng.md +751 -0
- package/ENG/perf-code-eng.md +441 -0
- package/ENG/premium-sites-studio-eng.md +320 -0
- package/ENG/sec-code-eng.md +706 -0
- package/ENG/test-code-eng.md +257 -0
- package/EXECUTION_STATE.md +107 -0
- package/GUIDE_ROUTER.md +274 -0
- package/LICENSE +21 -0
- package/LICENSE-DOCS.md +13 -0
- package/LOOP_ENGINEERING.md +551 -0
- package/LOOP_SYSTEM_DESIGN.md +394 -0
- package/ORCHESTRATOR_INTEGRATION.md +106 -0
- package/PROJECT_PROFILE.md +124 -0
- package/QUALITY_SCORECARD.md +54 -0
- package/README.md +492 -0
- package/TERMINOLOGY.md +21 -0
- package/THIRD_PARTY_NOTICES.md +129 -0
- package/THREAT_MODEL.md +35 -0
- package/package.json +51 -0
- package/schemas/delegated-result.schema.json +33 -0
- package/schemas/evidence.schema.json +15 -0
- package/schemas/execution-receipt.schema.json +46 -0
- package/schemas/routing-input.schema.json +17 -0
- package/schemas/routing-result.schema.json +17 -0
- package/schemas/task-brief.schema.json +24 -0
- package/schemas/work-state.schema.json +46 -0
- package/src/cli.js +341 -0
- package/src/commands/clear-state.js +11 -0
- package/src/commands/doctor.js +165 -0
- package/src/commands/init.js +42 -0
- package/src/commands/inspect.js +17 -0
- package/src/commands/route.js +32 -0
- package/src/commands/status.js +29 -0
- package/src/commands/update.js +109 -0
- package/src/commands/validate-protocol.js +133 -0
- package/src/commands/validate-receipt.js +19 -0
- package/src/commands/validate-state.js +30 -0
- package/src/core/agent-support.js +89 -0
- package/src/core/conformance.js +133 -0
- package/src/core/delegation.js +283 -0
- package/src/core/evidence.js +56 -0
- package/src/core/filesystem.js +122 -0
- package/src/core/inspect.js +115 -0
- package/src/core/json-safety.js +54 -0
- package/src/core/manifest.js +75 -0
- package/src/core/protocol.js +81 -0
- package/src/core/receipt.js +129 -0
- package/src/core/repository.js +19 -0
- package/src/core/router.js +296 -0
- package/src/core/schema-validation.js +179 -0
- package/src/core/templates.js +56 -0
- package/src/core/work-state.js +471 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { GUIDE_IDS, PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
|
|
3
|
+
export const ROUTING_SCHEMA_VERSION = 1;
|
|
4
|
+
|
|
5
|
+
const WORK_TYPES = new Set([
|
|
6
|
+
"documentation",
|
|
7
|
+
"code",
|
|
8
|
+
"bug",
|
|
9
|
+
"refactor",
|
|
10
|
+
"backend",
|
|
11
|
+
"api",
|
|
12
|
+
"api-auth",
|
|
13
|
+
"complete-website",
|
|
14
|
+
"mobile-ui",
|
|
15
|
+
"web-game",
|
|
16
|
+
"html-video",
|
|
17
|
+
"infrastructure",
|
|
18
|
+
"security-review",
|
|
19
|
+
"performance",
|
|
20
|
+
"accessibility",
|
|
21
|
+
"test-only",
|
|
22
|
+
"dependency-update",
|
|
23
|
+
"release",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const SIGNALS = Object.freeze({
|
|
27
|
+
surfaces: new Set([
|
|
28
|
+
"ui",
|
|
29
|
+
"forms",
|
|
30
|
+
"api",
|
|
31
|
+
"auth",
|
|
32
|
+
"data",
|
|
33
|
+
"database",
|
|
34
|
+
"mobile",
|
|
35
|
+
"desktop",
|
|
36
|
+
"game",
|
|
37
|
+
"video",
|
|
38
|
+
"ci",
|
|
39
|
+
"config",
|
|
40
|
+
"critical-path",
|
|
41
|
+
]),
|
|
42
|
+
risks: new Set([
|
|
43
|
+
"untrusted-input",
|
|
44
|
+
"personal-data",
|
|
45
|
+
"secrets",
|
|
46
|
+
"external-service",
|
|
47
|
+
"publication",
|
|
48
|
+
"critical-path",
|
|
49
|
+
"performance",
|
|
50
|
+
"accessibility",
|
|
51
|
+
]),
|
|
52
|
+
platforms: new Set(["web", "mobile", "desktop", "server", "ci", "cross-platform"]),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const PLATFORM_SEMANTICS = Object.freeze({
|
|
56
|
+
web: Object.freeze({
|
|
57
|
+
mode: "informational-only",
|
|
58
|
+
description: "Web is recorded as context; surface, risk, and work signals select guides.",
|
|
59
|
+
}),
|
|
60
|
+
mobile: Object.freeze({
|
|
61
|
+
mode: "contextual",
|
|
62
|
+
reason: "PLATFORM_MOBILE",
|
|
63
|
+
description: "Mobile UI context adds performance guidance and reinforces design/accessibility.",
|
|
64
|
+
}),
|
|
65
|
+
desktop: Object.freeze({
|
|
66
|
+
mode: "contextual",
|
|
67
|
+
reason: "PLATFORM_DESKTOP",
|
|
68
|
+
description: "Desktop UI context reinforces design and accessibility guidance.",
|
|
69
|
+
}),
|
|
70
|
+
server: Object.freeze({
|
|
71
|
+
mode: "contextual",
|
|
72
|
+
reason: "PLATFORM_SERVER",
|
|
73
|
+
description: "Server authentication context adds testing and reinforces the trust boundary.",
|
|
74
|
+
}),
|
|
75
|
+
ci: Object.freeze({
|
|
76
|
+
mode: "contextual",
|
|
77
|
+
reason: "PLATFORM_CI",
|
|
78
|
+
description: "Executable CI changes add security review to the existing change checks.",
|
|
79
|
+
}),
|
|
80
|
+
"cross-platform": Object.freeze({
|
|
81
|
+
mode: "informational-only",
|
|
82
|
+
description: "Cross-platform is recorded as context; it does not select a guide by itself.",
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const WORK_GUIDES = Object.freeze({
|
|
87
|
+
"complete-website": ["premium", "design", "accessibility", "clean", "test", "security", "performance"],
|
|
88
|
+
"api-auth": ["clean", "test", "security", "performance"],
|
|
89
|
+
api: ["clean", "test"],
|
|
90
|
+
backend: ["clean", "test"],
|
|
91
|
+
code: ["clean", "test"],
|
|
92
|
+
bug: ["clean", "test"],
|
|
93
|
+
refactor: ["clean", "test"],
|
|
94
|
+
"dependency-update": ["clean", "test"],
|
|
95
|
+
release: ["clean", "test"],
|
|
96
|
+
"mobile-ui": ["clean", "test", "design", "accessibility", "security", "performance"],
|
|
97
|
+
"web-game": ["games", "clean", "test", "security", "performance", "accessibility"],
|
|
98
|
+
"html-video": ["design", "accessibility", "performance", "test", "security"],
|
|
99
|
+
infrastructure: ["security", "test"],
|
|
100
|
+
"security-review": ["security"],
|
|
101
|
+
performance: ["performance", "test"],
|
|
102
|
+
accessibility: ["accessibility", "test"],
|
|
103
|
+
"test-only": ["test"],
|
|
104
|
+
documentation: [],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const PRIMARY_GUIDES = Object.freeze({
|
|
108
|
+
"complete-website": "premium",
|
|
109
|
+
"web-game": "games",
|
|
110
|
+
documentation: null,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export class RouteInputError extends Error {
|
|
114
|
+
constructor(message) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = "RouteInputError";
|
|
117
|
+
this.code = "ROUTING_FAILURE";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function reasonForWorkType(workType) {
|
|
122
|
+
return `WORK_${workType.toUpperCase().replaceAll("-", "_")}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function reasonForSignal(prefix, signal) {
|
|
126
|
+
return `${prefix}_${signal.toUpperCase().replaceAll("-", "_")}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeArray(value, name, allowed) {
|
|
130
|
+
if (value === undefined) return [];
|
|
131
|
+
if (!Array.isArray(value)) throw new RouteInputError(`${name} must be an array`);
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
for (const item of value) {
|
|
134
|
+
if (typeof item !== "string" || !allowed.has(item)) {
|
|
135
|
+
throw new RouteInputError(`Unknown ${name.slice(0, -1)}: ${item}`);
|
|
136
|
+
}
|
|
137
|
+
if (seen.has(item)) throw new RouteInputError(`Duplicate ${name.slice(0, -1)}: ${item}`);
|
|
138
|
+
seen.add(item);
|
|
139
|
+
}
|
|
140
|
+
return [...seen].sort();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function normalizeRouteInput(input = {}) {
|
|
144
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
145
|
+
throw new RouteInputError("Route input must be an object");
|
|
146
|
+
}
|
|
147
|
+
if (typeof input.workType !== "string" || !WORK_TYPES.has(input.workType)) {
|
|
148
|
+
throw new RouteInputError(`Unknown work type: ${input.workType}`);
|
|
149
|
+
}
|
|
150
|
+
for (const key of ["behaviorChange", "executableChange"]) {
|
|
151
|
+
if (input[key] !== undefined && typeof input[key] !== "boolean") {
|
|
152
|
+
throw new RouteInputError(`${key} must be boolean`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
schemaVersion: ROUTING_SCHEMA_VERSION,
|
|
157
|
+
workType: input.workType,
|
|
158
|
+
surfaces: normalizeArray(input.surfaces, "surfaces", SIGNALS.surfaces),
|
|
159
|
+
risks: normalizeArray(input.risks, "risks", SIGNALS.risks),
|
|
160
|
+
platforms: normalizeArray(input.platforms, "platforms", SIGNALS.platforms),
|
|
161
|
+
behaviorChange: input.behaviorChange ?? false,
|
|
162
|
+
executableChange: input.executableChange ?? false,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function evaluateRoute(input = {}) {
|
|
167
|
+
const normalized = normalizeRouteInput(input);
|
|
168
|
+
const selected = new Map();
|
|
169
|
+
const excluded = {};
|
|
170
|
+
|
|
171
|
+
function add(guide, reason) {
|
|
172
|
+
if (!GUIDE_IDS.includes(guide)) throw new RouteInputError(`Unknown guide: ${guide}`);
|
|
173
|
+
const reasons = selected.get(guide) ?? [];
|
|
174
|
+
if (!reasons.includes(reason)) reasons.push(reason);
|
|
175
|
+
selected.set(guide, reasons);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const workReason = reasonForWorkType(normalized.workType);
|
|
179
|
+
for (const guide of WORK_GUIDES[normalized.workType]) add(guide, workReason);
|
|
180
|
+
|
|
181
|
+
if (normalized.surfaces.includes("ui") || normalized.surfaces.includes("forms")) {
|
|
182
|
+
add("design", "SURFACE_UI");
|
|
183
|
+
add("accessibility", normalized.surfaces.includes("forms") ? "SURFACE_FORMS" : "SURFACE_UI");
|
|
184
|
+
}
|
|
185
|
+
if (normalized.surfaces.includes("mobile") || normalized.surfaces.includes("desktop")) {
|
|
186
|
+
add("design", "SURFACE_PLATFORM_UI");
|
|
187
|
+
add("accessibility", "SURFACE_PLATFORM_UI");
|
|
188
|
+
}
|
|
189
|
+
if (normalized.surfaces.includes("game")) add("games", "SURFACE_GAME");
|
|
190
|
+
if (normalized.surfaces.includes("video")) {
|
|
191
|
+
add("design", "SURFACE_VIDEO");
|
|
192
|
+
add("accessibility", "SURFACE_VIDEO");
|
|
193
|
+
}
|
|
194
|
+
if (normalized.surfaces.includes("auth")) add("security", "SURFACE_AUTH");
|
|
195
|
+
|
|
196
|
+
for (const risk of normalized.risks) {
|
|
197
|
+
if (["untrusted-input", "personal-data", "secrets", "external-service", "publication"].includes(risk)) {
|
|
198
|
+
add("security", reasonForSignal("RISK", risk));
|
|
199
|
+
}
|
|
200
|
+
if (["critical-path", "performance"].includes(risk)) {
|
|
201
|
+
add("performance", reasonForSignal("RISK", risk));
|
|
202
|
+
}
|
|
203
|
+
if (risk === "accessibility") add("accessibility", "RISK_ACCESSIBILITY");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (normalized.behaviorChange) {
|
|
207
|
+
add("clean", "CHANGE_BEHAVIOR");
|
|
208
|
+
add("test", "CHANGE_BEHAVIOR");
|
|
209
|
+
}
|
|
210
|
+
if (normalized.executableChange) {
|
|
211
|
+
add("clean", "CHANGE_EXECUTABLE_CONFIG");
|
|
212
|
+
add("test", "CHANGE_EXECUTABLE_CONFIG");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const hasUiContext = normalized.surfaces.some((surface) => ["ui", "forms", "mobile", "desktop"].includes(surface));
|
|
216
|
+
if (normalized.platforms.includes("mobile") && hasUiContext) {
|
|
217
|
+
add("design", "PLATFORM_MOBILE");
|
|
218
|
+
add("accessibility", "PLATFORM_MOBILE");
|
|
219
|
+
add("performance", "PLATFORM_MOBILE");
|
|
220
|
+
}
|
|
221
|
+
if (normalized.platforms.includes("desktop") && hasUiContext) {
|
|
222
|
+
add("design", "PLATFORM_DESKTOP");
|
|
223
|
+
add("accessibility", "PLATFORM_DESKTOP");
|
|
224
|
+
}
|
|
225
|
+
if (normalized.platforms.includes("server") && normalized.surfaces.includes("auth")) {
|
|
226
|
+
add("security", "PLATFORM_SERVER");
|
|
227
|
+
add("test", "PLATFORM_SERVER");
|
|
228
|
+
}
|
|
229
|
+
if (normalized.platforms.includes("ci") && normalized.executableChange) {
|
|
230
|
+
add("security", "PLATFORM_CI");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (normalized.workType === "documentation" && selected.size === 0) {
|
|
234
|
+
excluded.documentation = ["DOCUMENTATION_DOMAIN_GUIDE_REQUIRED"];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const guide of GUIDE_IDS) {
|
|
238
|
+
if (selected.has(guide)) continue;
|
|
239
|
+
if (guide === "security") excluded[guide] = ["NO_TRUST_BOUNDARY"];
|
|
240
|
+
else if (guide === "performance") excluded[guide] = ["NO_MEASURABLE_PERFORMANCE_RISK"];
|
|
241
|
+
else if (guide === "design" || guide === "accessibility") excluded[guide] = ["NO_UI_SURFACE"];
|
|
242
|
+
else if (guide === "premium" || guide === "games") excluded[guide] = ["NO_PRIMARY_WORK_TYPE"];
|
|
243
|
+
else excluded[guide] = ["NO_BEHAVIOR_OR_EXECUTABLE_CHANGE"];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const guides = [...selected.keys()];
|
|
247
|
+
const result = {
|
|
248
|
+
schemaVersion: ROUTING_SCHEMA_VERSION,
|
|
249
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
250
|
+
input: normalized,
|
|
251
|
+
primary: Object.prototype.hasOwnProperty.call(PRIMARY_GUIDES, normalized.workType)
|
|
252
|
+
? PRIMARY_GUIDES[normalized.workType]
|
|
253
|
+
: guides[0] ?? null,
|
|
254
|
+
guides,
|
|
255
|
+
reasons: Object.fromEntries(guides.map((guide) => [guide, selected.get(guide)])),
|
|
256
|
+
excluded,
|
|
257
|
+
};
|
|
258
|
+
return assertRouteInvariants(result);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function assertRouteInvariants(result) {
|
|
262
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
263
|
+
throw new RouteInputError("Route result must be an object");
|
|
264
|
+
}
|
|
265
|
+
if (!Array.isArray(result.guides) || new Set(result.guides).size !== result.guides.length) {
|
|
266
|
+
throw new RouteInputError("Route result contains duplicate guides");
|
|
267
|
+
}
|
|
268
|
+
for (const guide of result.guides) {
|
|
269
|
+
if (!GUIDE_IDS.includes(guide)) throw new RouteInputError(`Route result contains unknown guide: ${guide}`);
|
|
270
|
+
if (!Array.isArray(result.reasons?.[guide]) || result.reasons[guide].length === 0) {
|
|
271
|
+
throw new RouteInputError(`Selected guide has no reason: ${guide}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const [guide, reasons] of Object.entries(result.excluded ?? {})) {
|
|
275
|
+
if (guide !== "documentation" && !GUIDE_IDS.includes(guide)) {
|
|
276
|
+
throw new RouteInputError(`Route result contains unknown excluded guide: ${guide}`);
|
|
277
|
+
}
|
|
278
|
+
if (result.guides.includes(guide)) {
|
|
279
|
+
throw new RouteInputError(`Guide cannot be selected and excluded: ${guide}`);
|
|
280
|
+
}
|
|
281
|
+
if (!Array.isArray(reasons) || reasons.length === 0) {
|
|
282
|
+
throw new RouteInputError(`Excluded guide has no exclusion reason: ${guide}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (result.primary !== null && !result.guides.includes(result.primary)) {
|
|
286
|
+
throw new RouteInputError("Primary guide must be null or selected");
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const ROUTING_SIGNALS = Object.freeze({
|
|
292
|
+
workTypes: Object.freeze([...WORK_TYPES].sort()),
|
|
293
|
+
surfaces: Object.freeze([...SIGNALS.surfaces].sort()),
|
|
294
|
+
risks: Object.freeze([...SIGNALS.risks].sort()),
|
|
295
|
+
platforms: Object.freeze([...SIGNALS.platforms].sort()),
|
|
296
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getPackageRoot } from "./templates.js";
|
|
5
|
+
import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
|
|
6
|
+
import { createEvidence } from "./evidence.js";
|
|
7
|
+
|
|
8
|
+
export const SHIPPED_SCHEMA_NAMES = Object.freeze([
|
|
9
|
+
"routing-input",
|
|
10
|
+
"routing-result",
|
|
11
|
+
"work-state",
|
|
12
|
+
"execution-receipt",
|
|
13
|
+
"task-brief",
|
|
14
|
+
"delegated-result",
|
|
15
|
+
"evidence",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export class SchemaValidationError extends Error {
|
|
19
|
+
constructor(errors, label = "value") {
|
|
20
|
+
const normalized = errors.length > 0 ? errors : [`${label}: schema validation failed`];
|
|
21
|
+
super(normalized.join("; "));
|
|
22
|
+
this.name = "SchemaValidationError";
|
|
23
|
+
this.errors = normalized;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function typeMatches(value, type) {
|
|
28
|
+
if (type === "object") return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
29
|
+
if (type === "array") return Array.isArray(value);
|
|
30
|
+
if (type === "integer") return Number.isInteger(value);
|
|
31
|
+
if (type === "number") return typeof value === "number" && Number.isFinite(value);
|
|
32
|
+
if (type === "null") return value === null;
|
|
33
|
+
return typeof value === type;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function display(value) {
|
|
37
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validate(value, schema, location, errors) {
|
|
41
|
+
if (schema.const !== undefined && value !== schema.const) {
|
|
42
|
+
errors.push(`${location}: expected ${display(schema.const)}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
|
|
47
|
+
errors.push(`${location}: expected one of ${schema.enum.map(display).join(", ")}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (schema.oneOf) {
|
|
52
|
+
const matches = schema.oneOf.filter((candidate) => {
|
|
53
|
+
const candidateErrors = [];
|
|
54
|
+
validate(value, candidate, location, candidateErrors);
|
|
55
|
+
return candidateErrors.length === 0;
|
|
56
|
+
});
|
|
57
|
+
if (matches.length !== 1) errors.push(`${location}: expected exactly one matching schema`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (schema.type && !typeMatches(value, schema.type)) {
|
|
62
|
+
errors.push(`${location}: expected type ${schema.type}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof value === "string") {
|
|
67
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
68
|
+
errors.push(`${location}: must contain at least ${schema.minLength} characters`);
|
|
69
|
+
}
|
|
70
|
+
if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
|
|
71
|
+
errors.push(`${location}: does not match the required pattern`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
77
|
+
errors.push(`${location}: must contain at least ${schema.minItems} items`);
|
|
78
|
+
}
|
|
79
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
80
|
+
errors.push(`${location}: must contain at most ${schema.maxItems} items`);
|
|
81
|
+
}
|
|
82
|
+
if (schema.items) {
|
|
83
|
+
value.forEach((item, index) => validate(item, schema.items, `${location}[${index}]`, errors));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
88
|
+
for (const key of schema.required ?? []) {
|
|
89
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
90
|
+
errors.push(`${location}.${key}: is required`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const properties = schema.properties ?? {};
|
|
95
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
96
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
97
|
+
validate(value[key], child, `${location}.${key}`, errors);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (schema.additionalProperties === false) {
|
|
102
|
+
for (const key of Object.keys(value)) {
|
|
103
|
+
if (!Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
104
|
+
errors.push(`${location}.${key}: additional property is not allowed`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
108
|
+
for (const key of Object.keys(value)) {
|
|
109
|
+
if (!Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
110
|
+
validate(value[key], schema.additionalProperties, `${location}.${key}`, errors);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function validateSchema(value, schema, { label = "$" } = {}) {
|
|
118
|
+
assertJsonLimits(value, label);
|
|
119
|
+
const errors = [];
|
|
120
|
+
validate(value, schema, label, errors);
|
|
121
|
+
return errors;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function assertSchema(value, schema, label = "value") {
|
|
125
|
+
const errors = validateSchema(value, schema, { label });
|
|
126
|
+
if (errors.length > 0) throw new SchemaValidationError(errors, label);
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function readSchema(name, packageRoot = getPackageRoot()) {
|
|
131
|
+
const filename = name.endsWith(".schema.json") ? name : `${name}.schema.json`;
|
|
132
|
+
const schemaPath = path.join(packageRoot, "schemas", filename);
|
|
133
|
+
return JSON.parse(await readFile(schemaPath, "utf8"));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function inspectSchemaHealth(packageRoot = getPackageRoot()) {
|
|
137
|
+
const schemas = [];
|
|
138
|
+
for (const name of SHIPPED_SCHEMA_NAMES) {
|
|
139
|
+
const filename = `${name}.schema.json`;
|
|
140
|
+
const schemaPath = path.join(packageRoot, "schemas", filename);
|
|
141
|
+
try {
|
|
142
|
+
const bytes = await readFile(schemaPath);
|
|
143
|
+
assertJsonBytes(bytes, filename);
|
|
144
|
+
const schema = JSON.parse(bytes.toString("utf8"));
|
|
145
|
+
assertJsonLimits(schema, filename);
|
|
146
|
+
const version = schema?.properties?.schemaVersion?.const ?? null;
|
|
147
|
+
let status = "valid";
|
|
148
|
+
let error = null;
|
|
149
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
150
|
+
status = "invalid";
|
|
151
|
+
error = "schema root must be an object";
|
|
152
|
+
} else if (version !== 1) {
|
|
153
|
+
status = "unsupported-version";
|
|
154
|
+
error = `schemaVersion ${version ?? "missing"} is not supported`;
|
|
155
|
+
}
|
|
156
|
+
schemas.push({ name, version, status, error });
|
|
157
|
+
} catch (caught) {
|
|
158
|
+
const status = caught.code === "ENOENT" ? "missing" : "invalid";
|
|
159
|
+
schemas.push({ name, version: null, status, error: caught.message });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const status = schemas.some((schema) => schema.status === "invalid")
|
|
163
|
+
? "invalid"
|
|
164
|
+
: schemas.some((schema) => schema.status === "missing")
|
|
165
|
+
? "missing"
|
|
166
|
+
: schemas.some((schema) => schema.status === "unsupported-version")
|
|
167
|
+
? "unsupported-version"
|
|
168
|
+
: "valid";
|
|
169
|
+
return {
|
|
170
|
+
version: 1,
|
|
171
|
+
status,
|
|
172
|
+
schemas,
|
|
173
|
+
evidence: [createEvidence({
|
|
174
|
+
kind: status === "valid" ? "OBSERVED" : status === "missing" ? "NOT_VERIFIED" : "BLOCKED",
|
|
175
|
+
source: "shipped target schemas",
|
|
176
|
+
result: status,
|
|
177
|
+
})],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
6
|
+
|
|
7
|
+
export const TEMPLATE_PATHS = [
|
|
8
|
+
".forgeloop/.gitignore",
|
|
9
|
+
"AGENTS.md",
|
|
10
|
+
"CLAUDE.md",
|
|
11
|
+
".cursor/rules/project-loop.mdc",
|
|
12
|
+
".github/copilot-instructions.md",
|
|
13
|
+
"LOOP_ENGINEERING.md",
|
|
14
|
+
"GUIDE_ROUTER.md",
|
|
15
|
+
"PROJECT_PROFILE.md",
|
|
16
|
+
"LOOP_SYSTEM_DESIGN.md",
|
|
17
|
+
"QUALITY_SCORECARD.md",
|
|
18
|
+
"TERMINOLOGY.md",
|
|
19
|
+
"EXECUTION_STATE.md",
|
|
20
|
+
"DELEGATION_PROTOCOL.md",
|
|
21
|
+
"ORCHESTRATOR_INTEGRATION.md",
|
|
22
|
+
"THREAT_MODEL.md",
|
|
23
|
+
"CONTRACT_COVERAGE.md",
|
|
24
|
+
"AGENT_COMPATIBILITY.md",
|
|
25
|
+
"THIRD_PARTY_NOTICES.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"LICENSE-DOCS.md",
|
|
28
|
+
"ENG/accessibility-eng.md",
|
|
29
|
+
"ENG/clean-code-eng.md",
|
|
30
|
+
"ENG/design-code-eng.md",
|
|
31
|
+
"ENG/games-code-design-web-eng.md",
|
|
32
|
+
"ENG/perf-code-eng.md",
|
|
33
|
+
"ENG/premium-sites-studio-eng.md",
|
|
34
|
+
"ENG/sec-code-eng.md",
|
|
35
|
+
"ENG/test-code-eng.md",
|
|
36
|
+
"schemas/routing-input.schema.json",
|
|
37
|
+
"schemas/routing-result.schema.json",
|
|
38
|
+
"schemas/work-state.schema.json",
|
|
39
|
+
"schemas/execution-receipt.schema.json",
|
|
40
|
+
"schemas/task-brief.schema.json",
|
|
41
|
+
"schemas/delegated-result.schema.json",
|
|
42
|
+
"schemas/evidence.schema.json",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export function getPackageRoot() {
|
|
46
|
+
return PACKAGE_ROOT;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function readTemplateEntries(packageRoot = PACKAGE_ROOT) {
|
|
50
|
+
return Promise.all(
|
|
51
|
+
TEMPLATE_PATHS.map(async (relativePath) => ({
|
|
52
|
+
relativePath,
|
|
53
|
+
bytes: await readFile(path.join(packageRoot, relativePath)),
|
|
54
|
+
})),
|
|
55
|
+
);
|
|
56
|
+
}
|