feat: initial commit - Jhonny Editor

- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
This commit is contained in:
João Henrique
2026-09-08 09:59:31 -04:00
commit b541f502ba
1507 changed files with 387650 additions and 0 deletions
+723
View File
@@ -0,0 +1,723 @@
import { createHash } from "node:crypto";
export const PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION = 1;
export const PROJECT_INTAKE_REPORT_SCHEMA_VERSION = 1;
export const MAX_PROJECT_INTAKE_ITEMS = 2_000;
export const MAX_PROJECT_INTAKE_RULES = 64;
// A clip can produce six independent findings (extension, frame rate, offline,
// proxy, path, and ambiguous organization), plus bounded project-level findings.
export const MAX_PROJECT_INTAKE_FINDINGS = 12_200;
// Premiere's footage interpretation can expose a measured decimal that drifts
// slightly from the source timebase. Keep canonical rates distinct, then allow
// a bounded tolerance for non-canonical host readings.
export const FRAME_RATE_CANONICAL_SNAP_TOLERANCE_FPS = 0.005;
export const FRAME_RATE_MATCH_TOLERANCE_FPS = 0.05;
export type IntakeCertainty = "observed" | "unavailable" | "not_checked";
export type IntakeSeverity = "error" | "warning" | "info";
export type IntakeStatus = "ready" | "needs_attention" | "incomplete";
export interface RequiredBinRule {
name: string;
parentPath?: string;
}
export interface ApprovedPathPrefix {
id: string;
prefix: string;
}
export interface OrganizationMatchRule {
filenamePrefixes?: string[];
extensions?: string[];
}
export interface OrganizationRule {
id: string;
destinationBinName: string;
match: OrganizationMatchRule;
colorIndex?: number;
}
export interface FacilityIntakeTemplate {
schemaVersion: typeof PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION;
id: string;
version: string;
requiredBins: RequiredBinRule[];
allowedExtensions: string[];
allowedFrameRates: number[];
proxyPolicy: "ignore" | "report_missing" | "require";
approvedPathPrefixes: ApprovedPathPrefix[];
requiredEvidence: Array<"extension" | "frame_rate" | "offline" | "proxy" | "path">;
organizationRules: OrganizationRule[];
}
export interface ProjectIntakeItem {
id: string;
name: string;
type: "clip" | "bin" | "sequence" | "other";
parentId?: string;
treePath?: string;
mediaPath?: string;
offline?: boolean;
hasProxy?: boolean;
frameRate?: number;
/** Derived during snapshot validation; never accepted as caller authority. */
frameRateUnsupported?: true;
}
export interface ProjectIntakeSnapshot {
project: {
id: string;
name?: string;
};
items: ProjectIntakeItem[];
truncated: boolean;
unavailableEvidence: string[];
}
export interface ProjectIntakeFinding {
code: string;
severity: IntakeSeverity;
certainty: IntakeCertainty;
itemId?: string;
expected?: Record<string, unknown>;
observed?: Record<string, unknown>;
}
export interface ProjectIntakeOrganizationPlan {
applied: false;
planDigest: string;
proposedBins: Array<{
name: string;
exists: boolean;
createIfApproved: boolean;
}>;
proposedMoves: Array<{
projectItemId: string;
expectedParentId?: string;
destinationBinName: string;
ruleId: string;
colorIndex?: number;
}>;
}
export interface ProjectIntakeReport {
schemaVersion: typeof PROJECT_INTAKE_REPORT_SCHEMA_VERSION;
applied: false;
status: IntakeStatus;
project: {
id: string;
name?: string;
revision: string;
};
template: {
id: string;
version: string;
digest: string;
};
capture: {
itemCount: number;
truncated: boolean;
pathDisclosure: "redacted" | "requested";
};
findings: ProjectIntakeFinding[];
organizationPlan: ProjectIntakeOrganizationPlan;
limitations: string[];
}
export interface BuildProjectIntakeOptions {
includePaths?: boolean;
}
const REQUIRED_EVIDENCE = new Set(["extension", "frame_rate", "offline", "proxy", "path"]);
const ITEM_TYPES = new Set(["clip", "bin", "sequence", "other"]);
function sha256(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
function stableStringify(value: unknown): string {
if (value === null || typeof value !== "object") return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
const record = value as Record<string, unknown>;
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
}
function digest(value: unknown): string {
return sha256(stableStringify(value));
}
function asRecord(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
return value as Record<string, unknown>;
}
function assertOnlyKeys(record: Record<string, unknown>, allowed: readonly string[], label: string): void {
for (const key of Object.keys(record)) {
if (!allowed.includes(key)) throw new Error(`${label}.${key} is not supported`);
}
}
function requiredText(value: unknown, label: string, maxLength: number): string {
if (typeof value !== "string") throw new Error(`${label} must be a string`);
const normalized = value.trim();
if (!normalized) throw new Error(`${label} must not be empty`);
if (normalized.length > maxLength) throw new Error(`${label} exceeds ${maxLength} characters`);
return normalized;
}
function optionalText(value: unknown, label: string, maxLength: number): string | undefined {
if (value === undefined) return undefined;
return requiredText(value, label, maxLength);
}
function optionalBoolean(value: unknown, label: string): boolean | undefined {
if (value === undefined) return undefined;
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
return value;
}
function boundedArray(value: unknown, label: string, maxItems: number): unknown[] {
if (value === undefined) return [];
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
if (value.length > maxItems) throw new Error(`${label} must contain at most ${maxItems} entries`);
return value;
}
function normalizeExtension(value: unknown, label: string): string {
const extension = requiredText(value, label, 32).replace(/^\.+/, "").toLocaleLowerCase();
if (!/^[a-z0-9]{1,16}$/.test(extension)) throw new Error(`${label} must be a simple file extension`);
return extension;
}
function normalizeExtensions(value: unknown, label: string): string[] {
const values = boundedArray(value, label, MAX_PROJECT_INTAKE_RULES);
const normalized = values.map((entry, index) => normalizeExtension(entry, `${label}[${index}]`));
if (new Set(normalized).size !== normalized.length) throw new Error(`${label} contains duplicate extensions`);
return normalized.sort();
}
function normalizePrefixes(value: unknown, label: string): string[] {
const values = boundedArray(value, label, MAX_PROJECT_INTAKE_RULES);
const normalized = values.map((entry, index) => requiredText(entry, `${label}[${index}]`, 255).toLocaleLowerCase());
if (new Set(normalized).size !== normalized.length) throw new Error(`${label} contains duplicate filename prefixes`);
return normalized.sort();
}
function normalizeFrameRates(value: unknown, label: string): number[] {
const values = boundedArray(value, label, MAX_PROJECT_INTAKE_RULES);
const normalized = values.map((entry, index) => {
if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 1 || entry > 240) {
throw new Error(`${label}[${index}] must be a finite frame rate from 1 through 240`);
}
return Number(entry.toFixed(6));
});
if (new Set(normalized).size !== normalized.length) throw new Error(`${label} contains duplicate frame rates`);
return normalized.sort((left, right) => left - right);
}
function normalizeTreePath(value: string): string {
return value
.replace(/\\/g, "/")
.split("/")
.map((part) => part.trim())
.filter(Boolean)
.join("/")
.toLocaleLowerCase();
}
function normalizeFilesystemPath(value: string): string {
return value.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/+$/, "").toLocaleLowerCase();
}
function fileExtension(item: ProjectIntakeItem): string | undefined {
const source = item.mediaPath ?? item.name;
const leaf = source.replace(/\\/g, "/").split("/").at(-1) ?? "";
const index = leaf.lastIndexOf(".");
if (index <= 0 || index === leaf.length - 1) return undefined;
const value = leaf.slice(index + 1).toLocaleLowerCase();
return /^[a-z0-9]{1,16}$/.test(value) ? value : undefined;
}
function parentTreePath(item: ProjectIntakeItem): string | undefined {
if (!item.treePath) return undefined;
const path = normalizeTreePath(item.treePath);
const parts = path.split("/");
if (parts.at(-1) === item.name.trim().toLocaleLowerCase()) parts.pop();
return parts.join("/") || undefined;
}
function pathEvidence(mediaPath: string, includePaths: boolean): Record<string, unknown> {
return {
mediaPathHash: sha256(normalizeFilesystemPath(mediaPath)),
...(includePaths ? { mediaPath } : {}),
};
}
function isRequired(template: FacilityIntakeTemplate, evidence: "extension" | "frame_rate" | "offline" | "proxy" | "path"): boolean {
return template.requiredEvidence.includes(evidence)
|| (evidence === "extension" && template.allowedExtensions.length > 0)
|| (evidence === "frame_rate" && template.allowedFrameRates.length > 0)
|| (evidence === "proxy" && template.proxyPolicy === "require")
|| (evidence === "path" && template.approvedPathPrefixes.length > 0);
}
const CANONICAL_FRAME_RATES = [
23.976, 24, 25, 29.97, 30, 47.952, 48, 50, 59.94, 60, 100, 119.88, 120,
];
function canonicalFrameRate(value: number): number | undefined {
return CANONICAL_FRAME_RATES.find((rate) =>
Math.abs(rate - value) <= FRAME_RATE_CANONICAL_SNAP_TOLERANCE_FPS);
}
function frameRateMatches(observed: number, allowed: number): boolean {
const observedCanonical = canonicalFrameRate(observed);
const allowedCanonical = canonicalFrameRate(allowed);
if (observedCanonical !== undefined || allowedCanonical !== undefined) {
if (observedCanonical !== undefined && allowedCanonical !== undefined) {
return observedCanonical === allowedCanonical;
}
}
return Math.abs(allowed - observed) <= FRAME_RATE_MATCH_TOLERANCE_FPS;
}
function findingSort(left: ProjectIntakeFinding, right: ProjectIntakeFinding): number {
const severity = { error: 0, warning: 1, info: 2 } as const;
return severity[left.severity] - severity[right.severity]
|| left.code.localeCompare(right.code)
|| (left.itemId ?? "").localeCompare(right.itemId ?? "")
|| stableStringify(left.expected).localeCompare(stableStringify(right.expected));
}
function pushFinding(findings: ProjectIntakeFinding[], finding: ProjectIntakeFinding): void {
if (findings.length >= MAX_PROJECT_INTAKE_FINDINGS) return;
findings.push(finding);
}
export function validateFacilityIntakeTemplate(value: unknown): FacilityIntakeTemplate {
const input = asRecord(value, "template");
assertOnlyKeys(input, [
"schemaVersion", "id", "version", "requiredBins", "allowedExtensions", "allowedFrameRates",
"proxyPolicy", "approvedPathPrefixes", "requiredEvidence", "organizationRules",
], "template");
if (input.schemaVersion !== PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION) {
throw new Error(`template.schemaVersion must be ${PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION}`);
}
const requiredBins = boundedArray(input.requiredBins, "template.requiredBins", MAX_PROJECT_INTAKE_RULES).map((entry, index) => {
const rule = asRecord(entry, `template.requiredBins[${index}]`);
assertOnlyKeys(rule, ["name", "parentPath"], `template.requiredBins[${index}]`);
return {
name: requiredText(rule.name, `template.requiredBins[${index}].name`, 255),
...(optionalText(rule.parentPath, `template.requiredBins[${index}].parentPath`, 1024) ? {
parentPath: optionalText(rule.parentPath, `template.requiredBins[${index}].parentPath`, 1024),
} : {}),
};
});
const binKeys = requiredBins.map((rule) => `${normalizeTreePath(rule.parentPath ?? "")}\u0000${rule.name.toLocaleLowerCase()}`);
if (new Set(binKeys).size !== binKeys.length) throw new Error("template.requiredBins contains duplicate bin requirements");
const approvedPathPrefixes = boundedArray(input.approvedPathPrefixes, "template.approvedPathPrefixes", MAX_PROJECT_INTAKE_RULES).map((entry, index) => {
const prefix = asRecord(entry, `template.approvedPathPrefixes[${index}]`);
assertOnlyKeys(prefix, ["id", "prefix"], `template.approvedPathPrefixes[${index}]`);
return {
id: requiredText(prefix.id, `template.approvedPathPrefixes[${index}].id`, 128),
prefix: requiredText(prefix.prefix, `template.approvedPathPrefixes[${index}].prefix`, 4096),
};
});
if (new Set(approvedPathPrefixes.map((prefix) => prefix.id)).size !== approvedPathPrefixes.length) {
throw new Error("template.approvedPathPrefixes contains duplicate ids");
}
const requiredEvidence = boundedArray(input.requiredEvidence, "template.requiredEvidence", 5).map((entry, index) => {
if (typeof entry !== "string" || !REQUIRED_EVIDENCE.has(entry)) {
throw new Error(`template.requiredEvidence[${index}] must be extension, frame_rate, offline, proxy, or path`);
}
return entry as FacilityIntakeTemplate["requiredEvidence"][number];
});
if (new Set(requiredEvidence).size !== requiredEvidence.length) throw new Error("template.requiredEvidence contains duplicate entries");
const organizationRules = boundedArray(input.organizationRules, "template.organizationRules", MAX_PROJECT_INTAKE_RULES).map((entry, index) => {
const rule = asRecord(entry, `template.organizationRules[${index}]`);
assertOnlyKeys(rule, ["id", "destinationBinName", "match", "colorIndex"], `template.organizationRules[${index}]`);
const match = asRecord(rule.match, `template.organizationRules[${index}].match`);
assertOnlyKeys(match, ["filenamePrefixes", "extensions"], `template.organizationRules[${index}].match`);
const filenamePrefixes = normalizePrefixes(match.filenamePrefixes, `template.organizationRules[${index}].match.filenamePrefixes`);
const extensions = normalizeExtensions(match.extensions, `template.organizationRules[${index}].match.extensions`);
if (!filenamePrefixes.length && !extensions.length) {
throw new Error(`template.organizationRules[${index}].match must include a filename prefix or extension`);
}
const colorIndex = rule.colorIndex;
if (colorIndex !== undefined && (!Number.isInteger(colorIndex) || typeof colorIndex !== "number" || colorIndex < 0 || colorIndex > 14)) {
throw new Error(`template.organizationRules[${index}].colorIndex must be an integer from 0 through 14`);
}
return {
id: requiredText(rule.id, `template.organizationRules[${index}].id`, 128),
destinationBinName: requiredText(rule.destinationBinName, `template.organizationRules[${index}].destinationBinName`, 255),
match: {
...(filenamePrefixes.length ? { filenamePrefixes } : {}),
...(extensions.length ? { extensions } : {}),
},
...(colorIndex === undefined ? {} : { colorIndex }),
};
});
if (new Set(organizationRules.map((rule) => rule.id)).size !== organizationRules.length) {
throw new Error("template.organizationRules contains duplicate ids");
}
const proxyPolicy = input.proxyPolicy === undefined ? "ignore" : input.proxyPolicy;
if (proxyPolicy !== "ignore" && proxyPolicy !== "report_missing" && proxyPolicy !== "require") {
throw new Error("template.proxyPolicy must be ignore, report_missing, or require");
}
return {
schemaVersion: PROJECT_INTAKE_TEMPLATE_SCHEMA_VERSION,
id: requiredText(input.id, "template.id", 128),
version: requiredText(input.version, "template.version", 128),
requiredBins: requiredBins.sort((left, right) => `${left.parentPath ?? ""}/${left.name}`.localeCompare(`${right.parentPath ?? ""}/${right.name}`)),
allowedExtensions: normalizeExtensions(input.allowedExtensions, "template.allowedExtensions"),
allowedFrameRates: normalizeFrameRates(input.allowedFrameRates, "template.allowedFrameRates"),
proxyPolicy,
approvedPathPrefixes: approvedPathPrefixes.sort((left, right) => left.id.localeCompare(right.id)),
requiredEvidence: requiredEvidence.sort(),
organizationRules: organizationRules.sort((left, right) => left.id.localeCompare(right.id)),
};
}
export function validateProjectIntakeSnapshot(value: unknown): ProjectIntakeSnapshot {
const input = asRecord(value, "snapshot");
assertOnlyKeys(input, ["project", "items", "truncated", "unavailableEvidence"], "snapshot");
const project = asRecord(input.project, "snapshot.project");
assertOnlyKeys(project, ["id", "name"], "snapshot.project");
const items = boundedArray(input.items, "snapshot.items", MAX_PROJECT_INTAKE_ITEMS).map((entry, index) => {
const item = asRecord(entry, `snapshot.items[${index}]`);
assertOnlyKeys(item, ["id", "name", "type", "parentId", "treePath", "mediaPath", "offline", "hasProxy", "frameRate"], `snapshot.items[${index}]`);
if (typeof item.type !== "string" || !ITEM_TYPES.has(item.type)) {
throw new Error(`snapshot.items[${index}].type must be clip, bin, sequence, or other`);
}
const frameRateSupported = item.frameRate === undefined
|| (typeof item.frameRate === "number" && Number.isFinite(item.frameRate) && item.frameRate >= 1 && item.frameRate <= 240);
return {
id: requiredText(item.id, `snapshot.items[${index}].id`, 512),
name: requiredText(item.name, `snapshot.items[${index}].name`, 255),
type: item.type as ProjectIntakeItem["type"],
...(optionalText(item.parentId, `snapshot.items[${index}].parentId`, 512) ? { parentId: optionalText(item.parentId, `snapshot.items[${index}].parentId`, 512) } : {}),
...(optionalText(item.treePath, `snapshot.items[${index}].treePath`, 4096) ? { treePath: optionalText(item.treePath, `snapshot.items[${index}].treePath`, 4096) } : {}),
...(optionalText(item.mediaPath, `snapshot.items[${index}].mediaPath`, 4096) ? { mediaPath: optionalText(item.mediaPath, `snapshot.items[${index}].mediaPath`, 4096) } : {}),
...(optionalBoolean(item.offline, `snapshot.items[${index}].offline`) === undefined ? {} : { offline: optionalBoolean(item.offline, `snapshot.items[${index}].offline`) }),
...(optionalBoolean(item.hasProxy, `snapshot.items[${index}].hasProxy`) === undefined ? {} : { hasProxy: optionalBoolean(item.hasProxy, `snapshot.items[${index}].hasProxy`) }),
...(item.frameRate === undefined || !frameRateSupported
? (frameRateSupported ? {} : { frameRateUnsupported: true as const })
: { frameRate: Number((item.frameRate as number).toFixed(6)) }),
};
});
if (new Set(items.map((item) => item.id)).size !== items.length) throw new Error("snapshot.items contains duplicate ids");
const unavailableEvidence = boundedArray(input.unavailableEvidence, "snapshot.unavailableEvidence", MAX_PROJECT_INTAKE_RULES).map((entry, index) =>
requiredText(entry, `snapshot.unavailableEvidence[${index}]`, 128),
);
if (new Set(unavailableEvidence).size !== unavailableEvidence.length) throw new Error("snapshot.unavailableEvidence contains duplicate entries");
if (typeof input.truncated !== "boolean") throw new Error("snapshot.truncated must be a boolean");
return {
project: {
id: requiredText(project.id, "snapshot.project.id", 512),
...(optionalText(project.name, "snapshot.project.name", 255) ? { name: optionalText(project.name, "snapshot.project.name", 255) } : {}),
},
items: items.sort((left, right) => left.id.localeCompare(right.id)),
truncated: input.truncated,
unavailableEvidence: unavailableEvidence.sort(),
};
}
function ruleMatches(item: ProjectIntakeItem, rule: OrganizationRule): boolean {
const name = item.name.toLocaleLowerCase();
const extension = fileExtension(item);
const prefixes = rule.match.filenamePrefixes ?? [];
const extensions = rule.match.extensions ?? [];
return (!prefixes.length || prefixes.some((prefix) => name.startsWith(prefix)))
&& (!extensions.length || (extension !== undefined && extensions.includes(extension)));
}
export function buildProjectIntakeReport(
snapshotValue: unknown,
templateValue: unknown,
options: BuildProjectIntakeOptions = {},
): ProjectIntakeReport {
const snapshot = validateProjectIntakeSnapshot(snapshotValue);
const template = validateFacilityIntakeTemplate(templateValue);
const includePaths = options.includePaths === true;
const templateDigest = digest(template);
const projectRevision = digest(snapshot);
const findings: ProjectIntakeFinding[] = [];
const requiredUnavailable = new Set<string>();
if (snapshot.truncated) {
pushFinding(findings, {
code: "CAPTURE_TRUNCATED",
severity: "error",
certainty: "unavailable",
expected: { maximumItems: MAX_PROJECT_INTAKE_ITEMS },
observed: { itemCount: snapshot.items.length },
});
}
for (const evidence of snapshot.unavailableEvidence) {
const normalized = evidence.toLocaleLowerCase();
if (REQUIRED_EVIDENCE.has(normalized) && isRequired(template, normalized as FacilityIntakeTemplate["requiredEvidence"][number])) {
requiredUnavailable.add(normalized);
pushFinding(findings, {
code: "REQUIRED_EVIDENCE_UNAVAILABLE",
severity: "error",
certainty: "unavailable",
expected: { evidence: normalized },
});
}
}
const bins = snapshot.items.filter((item) => item.type === "bin");
for (const rule of template.requiredBins) {
const nameMatches = bins.filter((item) => item.name.toLocaleLowerCase() === rule.name.toLocaleLowerCase());
if (!nameMatches.length) {
pushFinding(findings, {
code: "REQUIRED_BIN_MISSING",
severity: "error",
certainty: "observed",
expected: { name: rule.name, ...(rule.parentPath ? { parentPath: rule.parentPath } : {}) },
});
continue;
}
if (!rule.parentPath) continue;
const expectedParent = normalizeTreePath(rule.parentPath);
const pathKnown = nameMatches.some((item) => parentTreePath(item) !== undefined);
const parentMatches = nameMatches.filter((item) => {
const parent = parentTreePath(item);
return parent === expectedParent || parent?.endsWith(`/${expectedParent}`) === true;
});
if (parentMatches.length) continue;
if (!pathKnown) {
requiredUnavailable.add("bin_parent_path");
pushFinding(findings, {
code: "REQUIRED_BIN_PARENT_UNAVAILABLE",
severity: "error",
certainty: "unavailable",
expected: { name: rule.name, parentPath: rule.parentPath },
});
} else {
pushFinding(findings, {
code: "REQUIRED_BIN_WRONG_PARENT",
severity: "error",
certainty: "observed",
expected: { name: rule.name, parentPath: rule.parentPath },
observed: { matchingBinIds: nameMatches.map((item) => item.id).sort() },
});
}
}
const clips = snapshot.items.filter((item) => item.type === "clip");
for (const item of clips) {
const extension = fileExtension(item);
if (template.allowedExtensions.length || isRequired(template, "extension")) {
if (!extension) {
requiredUnavailable.add("extension");
pushFinding(findings, {
code: "EXTENSION_UNAVAILABLE",
severity: "error",
certainty: "unavailable",
itemId: item.id,
expected: { allowedExtensions: template.allowedExtensions },
});
} else if (!template.allowedExtensions.includes(extension)) {
pushFinding(findings, {
code: "EXTENSION_NOT_ALLOWED",
severity: "error",
certainty: "observed",
itemId: item.id,
expected: { allowedExtensions: template.allowedExtensions },
observed: { extension },
});
}
}
if (template.allowedFrameRates.length || isRequired(template, "frame_rate")) {
if (item.frameRateUnsupported === true) {
requiredUnavailable.add("frame_rate");
pushFinding(findings, {
code: "FRAME_RATE_UNSUPPORTED",
severity: "error",
certainty: "unavailable",
itemId: item.id,
expected: { allowedFrameRates: template.allowedFrameRates },
});
} else if (item.frameRate === undefined) {
requiredUnavailable.add("frame_rate");
pushFinding(findings, {
code: "FRAME_RATE_UNAVAILABLE",
severity: "error",
certainty: "unavailable",
itemId: item.id,
expected: { allowedFrameRates: template.allowedFrameRates },
});
} else if (!template.allowedFrameRates.some((rate) => frameRateMatches(item.frameRate!, rate))) {
pushFinding(findings, {
code: "FRAME_RATE_NOT_ALLOWED",
severity: "error",
certainty: "observed",
itemId: item.id,
expected: { allowedFrameRates: template.allowedFrameRates },
observed: { frameRate: item.frameRate },
});
}
}
if (item.offline === true) {
pushFinding(findings, {
code: "OFFLINE_MEDIA",
severity: "error",
certainty: "observed",
itemId: item.id,
observed: { offline: true },
});
} else if (item.offline === undefined && isRequired(template, "offline")) {
requiredUnavailable.add("offline");
pushFinding(findings, {
code: "OFFLINE_STATE_UNAVAILABLE",
severity: "error",
certainty: "unavailable",
itemId: item.id,
});
}
if (template.proxyPolicy !== "ignore" || isRequired(template, "proxy")) {
if (item.hasProxy === false) {
pushFinding(findings, {
code: "PROXY_MISSING",
severity: template.proxyPolicy === "require" ? "error" : "warning",
certainty: "observed",
itemId: item.id,
expected: { proxyPolicy: template.proxyPolicy },
observed: { hasProxy: false },
});
} else if (item.hasProxy === undefined) {
if (template.proxyPolicy === "require" || isRequired(template, "proxy")) {
requiredUnavailable.add("proxy");
pushFinding(findings, {
code: "PROXY_STATE_UNAVAILABLE",
severity: "error",
certainty: "unavailable",
itemId: item.id,
expected: { proxyPolicy: template.proxyPolicy },
});
} else {
pushFinding(findings, {
code: "PROXY_STATE_NOT_CHECKED",
severity: "info",
certainty: "not_checked",
itemId: item.id,
});
}
}
}
if (template.approvedPathPrefixes.length || isRequired(template, "path")) {
if (!item.mediaPath) {
requiredUnavailable.add("path");
pushFinding(findings, {
code: "MEDIA_PATH_UNAVAILABLE",
severity: "error",
certainty: "unavailable",
itemId: item.id,
expected: { approvedPathPrefixIds: template.approvedPathPrefixes.map((prefix) => prefix.id) },
});
} else {
const normalizedPath = normalizeFilesystemPath(item.mediaPath);
const matches = template.approvedPathPrefixes.filter((prefix) => normalizedPath === normalizeFilesystemPath(prefix.prefix)
|| normalizedPath.startsWith(`${normalizeFilesystemPath(prefix.prefix)}/`));
if (!matches.length) {
pushFinding(findings, {
code: "MEDIA_PATH_NOT_APPROVED",
severity: "error",
certainty: "observed",
itemId: item.id,
expected: { approvedPathPrefixIds: template.approvedPathPrefixes.map((prefix) => prefix.id) },
observed: pathEvidence(item.mediaPath, includePaths),
});
}
}
}
}
const existingBinNames = new Set(bins.map((item) => item.name.toLocaleLowerCase()));
const proposedMoves: ProjectIntakeOrganizationPlan["proposedMoves"] = [];
for (const item of clips) {
const matches = template.organizationRules.filter((rule) => ruleMatches(item, rule));
if (matches.length > 1) {
pushFinding(findings, {
code: "AMBIGUOUS_ORGANIZATION_RULES",
severity: "warning",
certainty: "observed",
itemId: item.id,
observed: { matchingRuleIds: matches.map((rule) => rule.id).sort() },
});
continue;
}
const rule = matches[0];
if (!rule) continue;
proposedMoves.push({
projectItemId: item.id,
...(item.parentId ? { expectedParentId: item.parentId } : {}),
destinationBinName: rule.destinationBinName,
ruleId: rule.id,
...(rule.colorIndex === undefined ? {} : { colorIndex: rule.colorIndex }),
});
}
proposedMoves.sort((left, right) => left.projectItemId.localeCompare(right.projectItemId));
const proposedBins = [...new Set(proposedMoves.map((move) => move.destinationBinName))]
.sort((left, right) => left.localeCompare(right))
.map((name) => ({
name,
exists: existingBinNames.has(name.toLocaleLowerCase()),
createIfApproved: !existingBinNames.has(name.toLocaleLowerCase()),
}));
const planDigest = digest({ templateDigest, projectRevision, proposedBins, proposedMoves });
findings.sort(findingSort);
const incomplete = snapshot.truncated || requiredUnavailable.size > 0;
const status: IntakeStatus = incomplete
? "incomplete"
: findings.some((finding) => finding.severity === "error" || finding.severity === "warning")
? "needs_attention"
: "ready";
return {
schemaVersion: PROJECT_INTAKE_REPORT_SCHEMA_VERSION,
applied: false,
status,
project: {
id: snapshot.project.id,
...(snapshot.project.name ? { name: snapshot.project.name } : {}),
revision: projectRevision,
},
template: { id: template.id, version: template.version, digest: templateDigest },
capture: {
itemCount: snapshot.items.length,
truncated: snapshot.truncated,
pathDisclosure: includePaths ? "requested" : "redacted",
},
findings,
organizationPlan: {
applied: false,
planDigest,
proposedBins,
proposedMoves,
},
limitations: [
"This is a read-only plan. It does not create bins, rename items, move media, apply labels, or change Premiere.",
...(snapshot.truncated ? ["The project capture was truncated, so this report is incomplete."] : []),
...(requiredUnavailable.size ? ["Required host evidence was unavailable; inspect the affected items before using this report for compliance decisions."] : []),
],
};
}