feat: add artifact schema registry
This commit is contained in:
@@ -1478,6 +1478,8 @@ Fatal:
|
|||||||
- `workspace_permissions`
|
- `workspace_permissions`
|
||||||
- `internal_state_corruption`
|
- `internal_state_corruption`
|
||||||
- `template_load_failed`
|
- `template_load_failed`
|
||||||
|
- `artifact_schema_unknown`
|
||||||
|
- `artifact_schema_load_failed`
|
||||||
- `migration_pending`
|
- `migration_pending`
|
||||||
- `config_invalid`
|
- `config_invalid`
|
||||||
|
|
||||||
|
|||||||
73
docs/schemas/artifacts/common/final-report@1.json
Normal file
73
docs/schemas/artifacts/common/final-report@1.json
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "common/final-report@1",
|
||||||
|
"title": "Devflow Final Report",
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"runId",
|
||||||
|
"templateHash",
|
||||||
|
"bindings",
|
||||||
|
"inputs",
|
||||||
|
"phases",
|
||||||
|
"approvals",
|
||||||
|
"findings",
|
||||||
|
"commands",
|
||||||
|
"artifacts",
|
||||||
|
"events",
|
||||||
|
"unresolved",
|
||||||
|
"endedAt",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"runId": { "type": "string", "format": "uuid" },
|
||||||
|
"templateHash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||||
|
"bindings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["roleId", "personaHash", "backend"],
|
||||||
|
"properties": {
|
||||||
|
"roleId": { "type": "string", "minLength": 1 },
|
||||||
|
"personaHash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
||||||
|
"backend": { "type": "string", "enum": ["codex", "claude", "fake"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inputs": { "type": "object" },
|
||||||
|
"phases": { "type": "array" },
|
||||||
|
"approvals": { "type": "array" },
|
||||||
|
"findings": { "type": "array" },
|
||||||
|
"commands": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["kind", "argv", "exit_code"],
|
||||||
|
"properties": {
|
||||||
|
"kind": { "type": "string", "minLength": 1 },
|
||||||
|
"argv": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"exit_code": { "type": ["integer", "null"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"artifacts": { "type": "array" },
|
||||||
|
"events": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["tail"],
|
||||||
|
"properties": {
|
||||||
|
"tail": {
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unresolved": { "type": "array" },
|
||||||
|
"endedAt": { "type": "string", "format": "utc-date-time" },
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["completed", "failed", "aborted"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
docs/schemas/artifacts/dev/phase-plan@1.json
Normal file
73
docs/schemas/artifacts/dev/phase-plan@1.json
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "dev/phase-plan@1",
|
||||||
|
"title": "Devflow Phase Plan",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["phases"],
|
||||||
|
"properties": {
|
||||||
|
"phases": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["key", "title", "objective", "roles"],
|
||||||
|
"properties": {
|
||||||
|
"key": { "type": "string", "minLength": 1 },
|
||||||
|
"title": { "type": "string", "minLength": 1 },
|
||||||
|
"objective": { "type": "string", "minLength": 1 },
|
||||||
|
"roles": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": { "type": "string", "minLength": 1 }
|
||||||
|
},
|
||||||
|
"expectedArtifact": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["path", "schema"],
|
||||||
|
"properties": {
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(?!.*:)(?!.*(?:^|/)\\.{1,2}(?:/|$))[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)*$"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-z0-9_-]*/[a-z][a-z0-9_-]*@[1-9][0-9]*$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string", "minLength": 1 }
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["id", "title", "role", "writeSet"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "minLength": 1 },
|
||||||
|
"title": { "type": "string", "minLength": 1 },
|
||||||
|
"role": { "type": "string", "minLength": 1 },
|
||||||
|
"writeSet": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(?!/)(?!.*:)(?!.*[!{}()\\[\\]\\\\\\r\\n])(?!.*(?:^|/)\\.{1,2}(?:/|$)).+$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependsOn": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string", "minLength": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
docs/schemas/artifacts/dev/spec@1.json
Normal file
38
docs/schemas/artifacts/dev/spec@1.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "dev/spec@1",
|
||||||
|
"title": "Devflow Development Specification",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["summary", "requirements", "acceptanceCriteria", "risks"],
|
||||||
|
"properties": {
|
||||||
|
"summary": { "type": "string", "minLength": 1 },
|
||||||
|
"requirements": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["id", "description"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_-]*$" },
|
||||||
|
"description": { "type": "string", "minLength": 1 },
|
||||||
|
"priority": { "type": "string", "enum": ["low", "medium", "high"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"acceptanceCriteria": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": { "type": "string", "minLength": 1 }
|
||||||
|
},
|
||||||
|
"risks": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string", "minLength": 1 }
|
||||||
|
},
|
||||||
|
"openQuestions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string", "minLength": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": "8.17.1",
|
||||||
"dotenv": "17.4.2",
|
"dotenv": "17.4.2",
|
||||||
"yaml": "2.6.1",
|
"yaml": "2.6.1",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
|
|||||||
366
packages/core/src/artifact-schema.test.ts
Normal file
366
packages/core/src/artifact-schema.test.ts
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
clearArtifactSchemaCacheForTests,
|
||||||
|
loadSchema,
|
||||||
|
validateArtifact,
|
||||||
|
} from "./artifact-schema.js";
|
||||||
|
import { DevflowError } from "./errors.js";
|
||||||
|
|
||||||
|
const artifactRoot = resolve(
|
||||||
|
dirname(fileURLToPath(import.meta.url)),
|
||||||
|
"../../../docs/schemas/artifacts",
|
||||||
|
);
|
||||||
|
const repoRoot = resolve(artifactRoot, "../../..");
|
||||||
|
const hash64 = "a".repeat(64);
|
||||||
|
const runId = "00000000-0000-4000-8000-000000000001";
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
|
||||||
|
describe("artifact schema registry", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
clearArtifactSchemaCacheForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads the first locked artifact schemas from docs/schemas/artifacts", () => {
|
||||||
|
clearArtifactSchemaCacheForTests();
|
||||||
|
|
||||||
|
expect(loadSchema("dev/spec@1", { root: artifactRoot })).toMatchObject({
|
||||||
|
$id: "dev/spec@1",
|
||||||
|
});
|
||||||
|
expect(loadSchema("dev/phase-plan@1", { root: artifactRoot })).toMatchObject({
|
||||||
|
$id: "dev/phase-plan@1",
|
||||||
|
});
|
||||||
|
expect(loadSchema("common/final-report@1", { root: artifactRoot })).toMatchObject({
|
||||||
|
$id: "common/final-report@1",
|
||||||
|
});
|
||||||
|
expect(Object.isFrozen(loadSchema("dev/spec@1", { root: artifactRoot }))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds the default schema root from package subdirectories", () => {
|
||||||
|
process.chdir(resolve(repoRoot, "packages/core"));
|
||||||
|
|
||||||
|
expect(loadSchema("dev/spec@1")).toMatchObject({ $id: "dev/spec@1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates dev/spec@1 artifacts and returns compact validation errors", () => {
|
||||||
|
expect(
|
||||||
|
validateArtifact(
|
||||||
|
"dev/spec@1",
|
||||||
|
{
|
||||||
|
summary: "Add a binding algorithm",
|
||||||
|
requirements: [{ id: "REQ-1", description: "Bind every role" }],
|
||||||
|
acceptanceCriteria: ["All roles have bindings"],
|
||||||
|
risks: [],
|
||||||
|
},
|
||||||
|
{ root: artifactRoot },
|
||||||
|
),
|
||||||
|
).toEqual({ ok: true });
|
||||||
|
|
||||||
|
const result = validateArtifact(
|
||||||
|
"dev/spec@1",
|
||||||
|
{
|
||||||
|
summary: "Missing requirements",
|
||||||
|
requirements: [],
|
||||||
|
acceptanceCriteria: [],
|
||||||
|
risks: [],
|
||||||
|
},
|
||||||
|
{ root: artifactRoot },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.errors.map((error) => error.keyword)).toContain("minItems");
|
||||||
|
expect(result.errors[0]).toMatchObject({
|
||||||
|
instancePath: expect.any(String),
|
||||||
|
schemaPath: expect.any(String),
|
||||||
|
params: expect.any(Object),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates dev/phase-plan@1 artifacts", () => {
|
||||||
|
expect(
|
||||||
|
validateArtifact(
|
||||||
|
"dev/phase-plan@1",
|
||||||
|
{
|
||||||
|
phases: [
|
||||||
|
{
|
||||||
|
key: "implement",
|
||||||
|
title: "Implement",
|
||||||
|
objective: "Implement the requested behavior",
|
||||||
|
roles: ["implementer"],
|
||||||
|
expectedArtifact: {
|
||||||
|
path: "artifacts/spec.json",
|
||||||
|
schema: "dev/spec@1",
|
||||||
|
},
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
id: "task-1",
|
||||||
|
title: "Edit code",
|
||||||
|
role: "implementer",
|
||||||
|
writeSet: ["packages/core/src/**", "**/*.ts"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ root: artifactRoot },
|
||||||
|
),
|
||||||
|
).toEqual({ ok: true });
|
||||||
|
|
||||||
|
const invalidSchemaId = validateArtifact(
|
||||||
|
"dev/phase-plan@1",
|
||||||
|
{
|
||||||
|
phases: [
|
||||||
|
{
|
||||||
|
key: "implement",
|
||||||
|
title: "Implement",
|
||||||
|
objective: "Implement the requested behavior",
|
||||||
|
roles: ["implementer"],
|
||||||
|
expectedArtifact: {
|
||||||
|
path: "artifacts/spec.json",
|
||||||
|
schema: "../secret@1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ root: artifactRoot },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(invalidSchemaId.ok).toBe(false);
|
||||||
|
if (!invalidSchemaId.ok) {
|
||||||
|
expect(invalidSchemaId.errors.map((error) => error.keyword)).toContain("pattern");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of [
|
||||||
|
"../../secrets.json",
|
||||||
|
"/etc/passwd",
|
||||||
|
"artifacts/../outside.json",
|
||||||
|
"C:/outside.json",
|
||||||
|
"ok\n/../../outside.json",
|
||||||
|
"ok\r/../../outside.json",
|
||||||
|
]) {
|
||||||
|
const invalidPath = validateArtifact(
|
||||||
|
"dev/phase-plan@1",
|
||||||
|
{
|
||||||
|
phases: [
|
||||||
|
{
|
||||||
|
key: "implement",
|
||||||
|
title: "Implement",
|
||||||
|
objective: "Implement the requested behavior",
|
||||||
|
roles: ["implementer"],
|
||||||
|
expectedArtifact: {
|
||||||
|
path,
|
||||||
|
schema: "dev/spec@1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ root: artifactRoot },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(invalidPath.ok).toBe(false);
|
||||||
|
if (!invalidPath.ok) {
|
||||||
|
expect(invalidPath.errors.map((error) => error.keyword)).toContain("pattern");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingWriteSet = validateArtifact(
|
||||||
|
"dev/phase-plan@1",
|
||||||
|
{
|
||||||
|
phases: [
|
||||||
|
{
|
||||||
|
key: "implement",
|
||||||
|
title: "Implement",
|
||||||
|
objective: "Implement the requested behavior",
|
||||||
|
roles: ["implementer"],
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
id: "task-1",
|
||||||
|
title: "Edit code",
|
||||||
|
role: "implementer",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ root: artifactRoot },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(missingWriteSet.ok).toBe(false);
|
||||||
|
if (!missingWriteSet.ok) {
|
||||||
|
expect(missingWriteSet.errors.map((error) => error.keyword)).toContain("required");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const writeSet of [
|
||||||
|
"../../**",
|
||||||
|
"/etc/**",
|
||||||
|
"src/../secrets/**",
|
||||||
|
"C:/outside/**",
|
||||||
|
"src\n/../../outside",
|
||||||
|
"src\r/../../outside",
|
||||||
|
"..\\outside\\**",
|
||||||
|
"!/etc/**",
|
||||||
|
"!../*",
|
||||||
|
"{../*,packages/core/src/**}",
|
||||||
|
"{..,packages}/**",
|
||||||
|
"@(../*)",
|
||||||
|
"!(../*)",
|
||||||
|
"src/[ab]/**",
|
||||||
|
]) {
|
||||||
|
const invalidWriteSet = validateArtifact(
|
||||||
|
"dev/phase-plan@1",
|
||||||
|
{
|
||||||
|
phases: [
|
||||||
|
{
|
||||||
|
key: "implement",
|
||||||
|
title: "Implement",
|
||||||
|
objective: "Implement the requested behavior",
|
||||||
|
roles: ["implementer"],
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
id: "task-1",
|
||||||
|
title: "Edit code",
|
||||||
|
role: "implementer",
|
||||||
|
writeSet: [writeSet],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ root: artifactRoot },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(invalidWriteSet.ok).toBe(false);
|
||||||
|
if (!invalidWriteSet.ok) {
|
||||||
|
expect(invalidWriteSet.errors.map((error) => error.keyword)).toContain("pattern");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates common/final-report@1 minimum fields", () => {
|
||||||
|
expect(
|
||||||
|
validateArtifact(
|
||||||
|
"common/final-report@1",
|
||||||
|
{
|
||||||
|
runId,
|
||||||
|
templateHash: hash64,
|
||||||
|
bindings: [{ roleId: "implementer", personaHash: hash64, backend: "fake" }],
|
||||||
|
inputs: {},
|
||||||
|
phases: [],
|
||||||
|
approvals: [],
|
||||||
|
findings: [],
|
||||||
|
commands: [{ kind: "test", argv: ["pnpm", "test"], exit_code: 0 }],
|
||||||
|
artifacts: [],
|
||||||
|
events: { tail: [] },
|
||||||
|
unresolved: [],
|
||||||
|
endedAt: "2026-05-09T00:00:00.000Z",
|
||||||
|
status: "completed",
|
||||||
|
},
|
||||||
|
{ root: artifactRoot },
|
||||||
|
),
|
||||||
|
).toEqual({ ok: true });
|
||||||
|
|
||||||
|
const result = validateArtifact(
|
||||||
|
"common/final-report@1",
|
||||||
|
{
|
||||||
|
runId: "not-a-uuid",
|
||||||
|
templateHash: hash64,
|
||||||
|
bindings: [{ roleId: "implementer", personaHash: hash64, backend: "fake" }],
|
||||||
|
inputs: {},
|
||||||
|
phases: [],
|
||||||
|
approvals: [],
|
||||||
|
findings: [],
|
||||||
|
commands: [],
|
||||||
|
artifacts: [],
|
||||||
|
events: { tail: [] },
|
||||||
|
unresolved: [],
|
||||||
|
endedAt: "2026-99-99T99:99:99Z",
|
||||||
|
status: "executing",
|
||||||
|
},
|
||||||
|
{ root: artifactRoot },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.errors.map((error) => error.keyword)).toEqual(
|
||||||
|
expect.arrayContaining(["format", "enum"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails fatally for unknown or malformed schema ids", () => {
|
||||||
|
expect(() => loadSchema("dev/unknown@1", { root: artifactRoot })).toThrow(DevflowError);
|
||||||
|
expect(() => loadSchema("../secret@1", { root: artifactRoot })).toThrow(
|
||||||
|
/artifact_schema_unknown/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails fatally when schema files are malformed", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "devflow-artifact-schemas-"));
|
||||||
|
const devDir = join(root, "dev");
|
||||||
|
mkdirSync(devDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(devDir, "bad@1.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
$id: "dev/wrong@1",
|
||||||
|
type: "object",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() => loadSchema("dev/bad@1", { root })).toThrow(/artifact_schema_load_failed/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wraps registry root, JSON parse, and path layout load failures", () => {
|
||||||
|
expect(() =>
|
||||||
|
loadSchema("dev/spec@1", { root: join(tmpdir(), "missing-artifact-root") }),
|
||||||
|
).toThrow(/artifact_schema_load_failed/);
|
||||||
|
|
||||||
|
const badJsonRoot = mkdtempSync(join(tmpdir(), "devflow-artifact-schemas-"));
|
||||||
|
mkdirSync(join(badJsonRoot, "dev"), { recursive: true });
|
||||||
|
writeFileSync(join(badJsonRoot, "dev", "bad@1.json"), "{");
|
||||||
|
expect(() => loadSchema("dev/bad@1", { root: badJsonRoot })).toThrow(
|
||||||
|
/artifact_schema_load_failed/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const badPathRoot = mkdtempSync(join(tmpdir(), "devflow-artifact-schemas-"));
|
||||||
|
mkdirSync(join(badPathRoot, "bad"), { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(badPathRoot, "bad", "schema.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
$id: "bad/schema",
|
||||||
|
type: "object",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(() => loadSchema("dev/missing@1", { root: badPathRoot })).toThrow(
|
||||||
|
/artifact_schema_load_failed/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not load schemas from a target-controlled cwd shadow root", () => {
|
||||||
|
const shadowRoot = mkdtempSync(join(tmpdir(), "devflow-shadow-schemas-"));
|
||||||
|
const shadowSchemaDir = join(shadowRoot, "docs", "schemas", "artifacts", "dev");
|
||||||
|
mkdirSync(shadowSchemaDir, { recursive: true });
|
||||||
|
writeFileSync(join(shadowRoot, "package.json"), JSON.stringify({ name: "devflow" }));
|
||||||
|
writeFileSync(
|
||||||
|
join(shadowSchemaDir, "spec@1.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
$id: "dev/spec@1",
|
||||||
|
title: "SHADOW",
|
||||||
|
type: "object",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
process.chdir(shadowRoot);
|
||||||
|
|
||||||
|
expect(loadSchema("dev/spec@1")).toMatchObject({ title: "Devflow Development Specification" });
|
||||||
|
});
|
||||||
|
});
|
||||||
388
packages/core/src/artifact-schema.ts
Normal file
388
packages/core/src/artifact-schema.ts
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
||||||
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import {
|
||||||
|
Ajv2020,
|
||||||
|
type ErrorObject,
|
||||||
|
type FormatDefinition,
|
||||||
|
type ValidateFunction,
|
||||||
|
} from "ajv/dist/2020.js";
|
||||||
|
|
||||||
|
import { DevflowError } from "./errors.js";
|
||||||
|
import type { JsonObject, JsonValue } from "./persona.js";
|
||||||
|
|
||||||
|
export type JsonSchema = JsonObject;
|
||||||
|
|
||||||
|
export interface ValidationError {
|
||||||
|
instancePath: string;
|
||||||
|
schemaPath: string;
|
||||||
|
keyword: string;
|
||||||
|
message?: string;
|
||||||
|
params: JsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtifactSchemaOptions {
|
||||||
|
root?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompiledArtifactSchema {
|
||||||
|
id: string;
|
||||||
|
schema: JsonSchema;
|
||||||
|
validate: ValidateFunction;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaIdPattern = /^[a-z][a-z0-9_-]*\/[a-z][a-z0-9_-]*@[1-9]\d*$/;
|
||||||
|
const schemaRootSegments = ["docs", "schemas", "artifacts"] as const;
|
||||||
|
const registries = new Map<string, Map<string, CompiledArtifactSchema>>();
|
||||||
|
|
||||||
|
export function loadSchema(id: string, options: ArtifactSchemaOptions = {}): JsonSchema {
|
||||||
|
assertSchemaId(id);
|
||||||
|
const schema = registryFor(options).get(id);
|
||||||
|
if (!schema) {
|
||||||
|
throw artifactSchemaUnknown(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema.schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateArtifact(
|
||||||
|
id: string,
|
||||||
|
data: unknown,
|
||||||
|
options: ArtifactSchemaOptions = {},
|
||||||
|
): { ok: true } | { ok: false; errors: ValidationError[] } {
|
||||||
|
assertSchemaId(id);
|
||||||
|
const schema = registryFor(options).get(id);
|
||||||
|
if (!schema) {
|
||||||
|
throw artifactSchemaUnknown(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.validate(data)) {
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
errors: (schema.validate.errors ?? []).map(toValidationError),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearArtifactSchemaCacheForTests(): void {
|
||||||
|
registries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function registryFor(options: ArtifactSchemaOptions): Map<string, CompiledArtifactSchema> {
|
||||||
|
const root = resolveRegistryRoot(options.root ?? findDefaultSchemaRoot());
|
||||||
|
const cached = registries.get(root);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = loadRegistry(root);
|
||||||
|
registries.set(root, registry);
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRegistryRoot(root: string): string {
|
||||||
|
try {
|
||||||
|
return realpathSync(resolve(root));
|
||||||
|
} catch (error) {
|
||||||
|
throw artifactSchemaLoadFailed(root, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDefaultSchemaRoot(): string {
|
||||||
|
const moduleDirectory = currentModuleDirectory();
|
||||||
|
if (moduleDirectory === undefined) {
|
||||||
|
throw artifactSchemaLoadFailed(
|
||||||
|
"default",
|
||||||
|
new Error("Could not resolve current module directory for artifact schemas"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageRoot = findCorePackageRoot(moduleDirectory);
|
||||||
|
if (packageRoot === undefined) {
|
||||||
|
throw artifactSchemaLoadFailed(
|
||||||
|
"default",
|
||||||
|
new Error("Could not find @devflow/core package root for artifact schemas"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(packageRoot, "../..", ...schemaRootSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentModuleDirectory(): string | undefined {
|
||||||
|
const stack = new Error().stack?.split("\n").slice(1) ?? [];
|
||||||
|
|
||||||
|
for (const line of stack) {
|
||||||
|
const match = line.match(/\(?((?:file:\/\/)?\/[^):]+\.(?:cjs|mjs|js|ts)):\d+:\d+\)?/);
|
||||||
|
if (!match?.[1]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = match[1].startsWith("file://") ? fileURLToPath(match[1]) : match[1];
|
||||||
|
return dirname(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCorePackageRoot(startDirectory: string): string | undefined {
|
||||||
|
let current = resolve(startDirectory);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (isCorePackageRoot(current)) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = dirname(current);
|
||||||
|
if (parent === current) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCorePackageRoot(directory: string): boolean {
|
||||||
|
const packageJsonPath = join(directory, "package.json");
|
||||||
|
if (!existsSync(packageJsonPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { name?: unknown };
|
||||||
|
return packageJson.name === "@devflow/core";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addArtifactFormats(ajv: Ajv2020): void {
|
||||||
|
ajv.addFormat("uuid", uuidFormat);
|
||||||
|
ajv.addFormat("utc-date-time", utcDateTimeFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uuidFormat: FormatDefinition<string> = {
|
||||||
|
type: "string",
|
||||||
|
validate: (value: string) =>
|
||||||
|
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value),
|
||||||
|
};
|
||||||
|
|
||||||
|
const utcDateTimeFormat: FormatDefinition<string> = {
|
||||||
|
type: "string",
|
||||||
|
validate: isUtcDateTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
function isUtcDateTime(value: string): boolean {
|
||||||
|
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3}))?Z$/);
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, yearText, monthText, dayText, hourText, minuteText, secondText, millisecondText] = match;
|
||||||
|
const year = Number(yearText);
|
||||||
|
const month = Number(monthText);
|
||||||
|
const day = Number(dayText);
|
||||||
|
const hour = Number(hourText);
|
||||||
|
const minute = Number(minuteText);
|
||||||
|
const second = Number(secondText);
|
||||||
|
const millisecond = millisecondText === undefined ? 0 : Number(millisecondText);
|
||||||
|
const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second, millisecond));
|
||||||
|
|
||||||
|
return (
|
||||||
|
date.getUTCFullYear() === year &&
|
||||||
|
date.getUTCMonth() === month - 1 &&
|
||||||
|
date.getUTCDate() === day &&
|
||||||
|
date.getUTCHours() === hour &&
|
||||||
|
date.getUTCMinutes() === minute &&
|
||||||
|
date.getUTCSeconds() === second &&
|
||||||
|
date.getUTCMilliseconds() === millisecond
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadRegistry(root: string): Map<string, CompiledArtifactSchema> {
|
||||||
|
const ajv = new Ajv2020({ allErrors: true, strict: true });
|
||||||
|
addArtifactFormats(ajv);
|
||||||
|
const schemas = new Map<string, JsonSchemaFile>();
|
||||||
|
|
||||||
|
for (const file of readSchemaFiles(root, root)) {
|
||||||
|
if (schemas.has(file.id)) {
|
||||||
|
throw artifactSchemaLoadFailed(file.id, new Error(`Duplicate artifact schema id ${file.id}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
schemas.set(file.id, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const registry = new Map<string, CompiledArtifactSchema>();
|
||||||
|
for (const file of schemas.values()) {
|
||||||
|
try {
|
||||||
|
registry.set(file.id, {
|
||||||
|
id: file.id,
|
||||||
|
schema: deepFreeze(file.schema),
|
||||||
|
validate: ajv.compile(file.schema),
|
||||||
|
path: file.path,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw artifactSchemaLoadFailed(file.id, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JsonSchemaFile {
|
||||||
|
id: string;
|
||||||
|
schema: JsonSchema;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSchemaFiles(root: string, directory: string): JsonSchemaFile[] {
|
||||||
|
const files: JsonSchemaFile[] = [];
|
||||||
|
let entryNames: string[];
|
||||||
|
try {
|
||||||
|
entryNames = readdirSync(directory).sort();
|
||||||
|
} catch (error) {
|
||||||
|
throw artifactSchemaLoadFailed(directory, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entryName of entryNames) {
|
||||||
|
const path = join(directory, entryName);
|
||||||
|
let stat: ReturnType<typeof lstatSync>;
|
||||||
|
try {
|
||||||
|
stat = lstatSync(path);
|
||||||
|
} catch (error) {
|
||||||
|
throw artifactSchemaLoadFailed(path, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.isSymbolicLink()) {
|
||||||
|
throw artifactSchemaLoadFailed(
|
||||||
|
entryName,
|
||||||
|
new Error("Artifact schema path must not be a symlink"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
files.push(...readSchemaFiles(root, path));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entryName.endsWith(".json")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canonicalPath = resolveSchemaFilePath(path);
|
||||||
|
const id = schemaIdFromPath(root, canonicalPath);
|
||||||
|
const parsed = parseSchemaFile(id, canonicalPath);
|
||||||
|
if (!isJsonObject(parsed)) {
|
||||||
|
throw artifactSchemaLoadFailed(id, new Error("Artifact schema must be a JSON object"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.$id !== id) {
|
||||||
|
throw artifactSchemaLoadFailed(id, new Error(`Artifact schema $id must equal ${id}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push({ id, schema: parsed, path: canonicalPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaIdFromPath(root: string, path: string) {
|
||||||
|
const relativePath = relative(root, path).split(sep).join("/");
|
||||||
|
const id = relativePath.replace(/\.json$/, "");
|
||||||
|
if (!schemaIdPattern.test(id)) {
|
||||||
|
throw artifactSchemaLoadFailed(id, new Error(`Invalid artifact schema path ${relativePath}`));
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertSchemaId(id: string) {
|
||||||
|
if (!schemaIdPattern.test(id)) {
|
||||||
|
throw artifactSchemaUnknown(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toValidationError(error: ErrorObject): ValidationError {
|
||||||
|
return {
|
||||||
|
instancePath: error.instancePath,
|
||||||
|
schemaPath: error.schemaPath,
|
||||||
|
keyword: error.keyword,
|
||||||
|
...(error.message === undefined ? {} : { message: error.message }),
|
||||||
|
params: toJsonObject(error.params),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toJsonObject(value: Record<string, unknown>): JsonObject {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).map(([key, childValue]) => [key, toJsonValue(childValue)]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toJsonValue(value: unknown): JsonValue {
|
||||||
|
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return Number.isFinite(value) ? value : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(toJsonValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJsonObject(value)) {
|
||||||
|
return toJsonObject(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isJsonObject(value: unknown): value is JsonObject {
|
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSchemaFilePath(path: string): string {
|
||||||
|
try {
|
||||||
|
return realpathSync(path);
|
||||||
|
} catch (error) {
|
||||||
|
throw artifactSchemaLoadFailed(path, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSchemaFile(id: string, path: string): unknown {
|
||||||
|
try {
|
||||||
|
return JSON.parse(readFileSync(path, "utf8")) as unknown;
|
||||||
|
} catch (error) {
|
||||||
|
throw artifactSchemaLoadFailed(id, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepFreeze<T extends JsonValue>(value: T): T {
|
||||||
|
if (value !== null && typeof value === "object") {
|
||||||
|
Object.freeze(value);
|
||||||
|
for (const child of Object.values(value)) {
|
||||||
|
deepFreeze(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function artifactSchemaUnknown(id: string) {
|
||||||
|
return new DevflowError(`artifact_schema_unknown:${id}`, {
|
||||||
|
class: "fatal",
|
||||||
|
code: "artifact_schema_unknown",
|
||||||
|
recoveryHint: `Add docs/schemas/artifacts/${id}.json or update the template schema id.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function artifactSchemaLoadFailed(id: string, cause: unknown) {
|
||||||
|
return new DevflowError(`artifact_schema_load_failed:${id}`, {
|
||||||
|
class: "fatal",
|
||||||
|
code: "artifact_schema_load_failed",
|
||||||
|
cause,
|
||||||
|
recoveryHint: "Fix the artifact schema JSON document.",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from "./artifact-schema.js";
|
||||||
export * from "./config.js";
|
export * from "./config.js";
|
||||||
export * from "./binding.js";
|
export * from "./binding.js";
|
||||||
export * from "./enums.js";
|
export * from "./enums.js";
|
||||||
|
|||||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -75,6 +75,9 @@ importers:
|
|||||||
|
|
||||||
packages/core:
|
packages/core:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
ajv:
|
||||||
|
specifier: 8.17.1
|
||||||
|
version: 8.17.1
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: 17.4.2
|
specifier: 17.4.2
|
||||||
version: 17.4.2
|
version: 17.4.2
|
||||||
@@ -1263,6 +1266,9 @@ packages:
|
|||||||
'@vitest/utils@2.1.8':
|
'@vitest/utils@2.1.8':
|
||||||
resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==}
|
resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==}
|
||||||
|
|
||||||
|
ajv@8.17.1:
|
||||||
|
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||||
|
|
||||||
ansi-regex@5.0.1:
|
ansi-regex@5.0.1:
|
||||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1510,6 +1516,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
fast-deep-equal@3.1.3:
|
||||||
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
|
fast-uri@3.1.2:
|
||||||
|
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -1573,6 +1585,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
json-schema-traverse@1.0.0:
|
||||||
|
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||||
|
|
||||||
lefthook-darwin-arm64@2.1.6:
|
lefthook-darwin-arm64@2.1.6:
|
||||||
resolution: {integrity: sha512-hyB7eeiX78BS66f70byTJacDLC/xV1vgMv9n+idFUsrM7J3Udd/ag9Ag5NP3t0eN0EqQqAtrNnt35EH01lxnRQ==}
|
resolution: {integrity: sha512-hyB7eeiX78BS66f70byTJacDLC/xV1vgMv9n+idFUsrM7J3Udd/ag9Ag5NP3t0eN0EqQqAtrNnt35EH01lxnRQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
@@ -1793,6 +1808,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
engines: {node: '>= 14.18.0'}
|
||||||
|
|
||||||
|
require-from-string@2.0.2:
|
||||||
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
resolve-from@5.0.0:
|
resolve-from@5.0.0:
|
||||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2779,6 +2798,13 @@ snapshots:
|
|||||||
loupe: 3.2.1
|
loupe: 3.2.1
|
||||||
tinyrainbow: 1.2.0
|
tinyrainbow: 1.2.0
|
||||||
|
|
||||||
|
ajv@8.17.1:
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
fast-uri: 3.1.2
|
||||||
|
json-schema-traverse: 1.0.0
|
||||||
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
ansi-regex@5.0.1: {}
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
ansi-regex@6.2.2: {}
|
ansi-regex@6.2.2: {}
|
||||||
@@ -3044,6 +3070,10 @@ snapshots:
|
|||||||
|
|
||||||
expect-type@1.3.0: {}
|
expect-type@1.3.0: {}
|
||||||
|
|
||||||
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
|
fast-uri@3.1.2: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
@@ -3106,6 +3136,8 @@ snapshots:
|
|||||||
|
|
||||||
joycon@3.1.1: {}
|
joycon@3.1.1: {}
|
||||||
|
|
||||||
|
json-schema-traverse@1.0.0: {}
|
||||||
|
|
||||||
lefthook-darwin-arm64@2.1.6:
|
lefthook-darwin-arm64@2.1.6:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -3279,6 +3311,8 @@ snapshots:
|
|||||||
|
|
||||||
readdirp@4.1.2: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
|
require-from-string@2.0.2: {}
|
||||||
|
|
||||||
resolve-from@5.0.0: {}
|
resolve-from@5.0.0: {}
|
||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user