940 lines
34 KiB
TypeScript
940 lines
34 KiB
TypeScript
import { readFile, readdir } from 'node:fs/promises';
|
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { parseRosterV2, validateRosterV2Semantics } from './roster-v2.js';
|
|
|
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const repositoryRoot = resolve(packageRoot, '..', '..');
|
|
const fleetDocs = join(repositoryRoot, 'docs', 'fleet');
|
|
const frameworkFleet = join(packageRoot, 'framework', 'fleet');
|
|
|
|
const REQUIRED_FLEET_PAGES = [
|
|
'README.md',
|
|
'concepts/desired-vs-observed-state.md',
|
|
'concepts/identity-class-runtime.md',
|
|
'concepts/role-authority-and-leases.md',
|
|
'concepts/generated-env-launch-chain.md',
|
|
'reference/roster-v2.schema.json',
|
|
'reference/roster-v2-fields.md',
|
|
'reference/cli.md',
|
|
'reference/role-classes.md',
|
|
'reference/lifecycle-transitions.md',
|
|
'reference/status-and-drift.md',
|
|
'how-to/create-update-delete-agent.md',
|
|
'how-to/start-stop-restart.md',
|
|
'how-to/configure-tess-interaction.md',
|
|
'how-to/configure-ultron-validator.md',
|
|
'how-to/customize-roles.md',
|
|
'operations/reconcile-and-recover.md',
|
|
'operations/env-quarantine.md',
|
|
'operations/systemd-tmux-troubleshooting.md',
|
|
'operations/backup-restore.md',
|
|
'operations/upgrade-assets.md',
|
|
'migration/v1-to-v2.md',
|
|
'migration/example-profile-disposition.md',
|
|
'migration/legacy-class-aliases.md',
|
|
] as const;
|
|
|
|
async function markdownFiles(root: string): Promise<string[]> {
|
|
const entries = await readdir(root, { withFileTypes: true });
|
|
const paths = await Promise.all(
|
|
entries.map(async (entry): Promise<string[]> => {
|
|
const path = join(root, entry.name);
|
|
if (entry.isDirectory()) return markdownFiles(path);
|
|
return extname(entry.name) === '.md' ? [path] : [];
|
|
}),
|
|
);
|
|
return paths.flat().sort();
|
|
}
|
|
|
|
function localMarkdownTargets(source: string): string[] {
|
|
const link =
|
|
/\[[^\]]*\]\(\s*(?:<([^>]+)>|((?:\\.|[^()\s]|\([^()]*\))+))(?:\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?\s*\)/g;
|
|
return [...source.matchAll(link)]
|
|
.map((match): string => match[1] ?? match[2] ?? '')
|
|
.filter(
|
|
(target): boolean =>
|
|
target !== '' &&
|
|
!target.startsWith('http://') &&
|
|
!target.startsWith('https://') &&
|
|
!target.startsWith('mailto:'),
|
|
);
|
|
}
|
|
|
|
function markdownHeadingAnchors(source: string): Set<string> {
|
|
const anchors = new Set<string>();
|
|
let fence: { readonly marker: string; readonly length: number } | undefined;
|
|
|
|
for (const line of source.split('\n')) {
|
|
const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})(.*)$/);
|
|
if (fence === undefined && fenceMatch !== null) {
|
|
const run = fenceMatch[1] ?? '';
|
|
fence = { marker: run[0] ?? '', length: run.length };
|
|
continue;
|
|
}
|
|
if (fence !== undefined) {
|
|
const closingRun = line.match(/^\s{0,3}(`{3,}|~{3,})\s*$/)?.[1];
|
|
if (
|
|
closingRun !== undefined &&
|
|
closingRun[0] === fence.marker &&
|
|
closingRun.length >= fence.length
|
|
) {
|
|
fence = undefined;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const heading = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/)?.[1];
|
|
if (heading === undefined) continue;
|
|
const base = heading
|
|
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
.replace(/<[^>]*>/g, '')
|
|
.replace(/[`*_~]/g, '')
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/[^\p{L}\p{N}\s-]/gu, '')
|
|
.replace(/\s+/g, '-');
|
|
let anchor = base;
|
|
let duplicate = 0;
|
|
while (anchors.has(anchor)) {
|
|
duplicate += 1;
|
|
anchor = `${base}-${duplicate}`;
|
|
}
|
|
anchors.add(anchor);
|
|
}
|
|
return anchors;
|
|
}
|
|
|
|
function markdownLinkViolations(
|
|
sourcePath: string,
|
|
source: string,
|
|
documents: Readonly<Record<string, string>>,
|
|
): string[] {
|
|
const violations: string[] = [];
|
|
for (const target of localMarkdownTargets(source)) {
|
|
const [encodedPath = '', encodedFragment] = target.split('#', 2);
|
|
const targetPath = decodeURIComponent(encodedPath);
|
|
const normalizedTarget = resolve('/', dirname(sourcePath), targetPath).slice(1);
|
|
const targetSource = documents[normalizedTarget];
|
|
if (targetSource === undefined) {
|
|
violations.push(`${sourcePath} -> ${target}: missing file`);
|
|
continue;
|
|
}
|
|
if (encodedFragment !== undefined) {
|
|
const fragment = decodeURIComponent(encodedFragment);
|
|
if (fragment === '' || !markdownHeadingAnchors(targetSource).has(fragment)) {
|
|
violations.push(`${sourcePath} -> ${target}: missing heading`);
|
|
}
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
type CodeSurfaceCategory = 'ConcreteCommand' | 'Synopsis' | 'DataProfile' | 'InlineLiteral';
|
|
|
|
interface CodeSurface {
|
|
readonly category: CodeSurfaceCategory;
|
|
readonly path: string;
|
|
readonly line: number;
|
|
readonly block?: number;
|
|
readonly info?: string;
|
|
readonly source: string;
|
|
}
|
|
|
|
interface SurfaceDiagnostic {
|
|
readonly path: string;
|
|
readonly line: number;
|
|
readonly block?: number;
|
|
readonly category?: CodeSurfaceCategory;
|
|
readonly code: string;
|
|
}
|
|
|
|
interface FenceProfile {
|
|
readonly path: string;
|
|
readonly block: number;
|
|
readonly info: string;
|
|
readonly category: Exclude<CodeSurfaceCategory, 'InlineLiteral'>;
|
|
readonly recordSchemas?: readonly string[];
|
|
}
|
|
|
|
const FENCE_PROFILES = [
|
|
{
|
|
path: 'FLEET-LAUNCH.md',
|
|
block: 1,
|
|
info: 'dotenv',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.DOTENV.FLEET_LAUNCH'],
|
|
},
|
|
{
|
|
path: 'TASKS.md',
|
|
block: 1,
|
|
info: 'text-table',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.TEXT_TABLE.FLEET_TASKS'],
|
|
},
|
|
{
|
|
path: 'backlog-conventions.md',
|
|
block: 1,
|
|
info: 'text-diagram',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.TEXT_DIAGRAM.BACKLOG_FLOW'],
|
|
},
|
|
{
|
|
path: 'backlog-conventions.md',
|
|
block: 2,
|
|
info: 'fleet-command',
|
|
category: 'ConcreteCommand',
|
|
recordSchemas: [
|
|
'CMD.BACKLOG.CREATE.1',
|
|
'CMD.BACKLOG.CREATE.2',
|
|
'CMD.BACKLOG.CLAIM',
|
|
'CMD.BACKLOG.COMPLETE',
|
|
'CMD.BACKLOG.LIST_READY',
|
|
'CMD.BACKLOG.RECLAIM',
|
|
],
|
|
},
|
|
{
|
|
path: 'f4-matrix-connector.md',
|
|
block: 1,
|
|
info: 'typescript',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.TYPESCRIPT.MATRIX_CONNECTOR_TYPE'],
|
|
},
|
|
{
|
|
path: 'f4-matrix-connector.md',
|
|
block: 2,
|
|
info: 'yaml',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.YAML.MATRIX_CONNECTOR_CONFIG'],
|
|
},
|
|
{
|
|
path: 'how-to/configure-tess-interaction.md',
|
|
block: 1,
|
|
info: 'yaml',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.YAML.INTERACTION_AGENT'],
|
|
},
|
|
{
|
|
path: 'how-to/configure-ultron-validator.md',
|
|
block: 1,
|
|
info: 'yaml',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.YAML.VALIDATOR_AGENT'],
|
|
},
|
|
{
|
|
path: 'how-to/create-update-delete-agent.md',
|
|
block: 1,
|
|
info: 'fleet-synopsis',
|
|
category: 'Synopsis',
|
|
recordSchemas: ['SYN.AGENT.GET', 'SYN.PLAN.CREATE', 'SYN.PLAN.UPDATE', 'SYN.PLAN.DELETE'],
|
|
},
|
|
{
|
|
path: 'how-to/create-update-delete-agent.md',
|
|
block: 2,
|
|
info: 'fleet-command',
|
|
category: 'ConcreteCommand',
|
|
recordSchemas: ['CMD.AGENT.CREATE_JSON'],
|
|
},
|
|
{
|
|
path: 'how-to/create-update-delete-agent.md',
|
|
block: 3,
|
|
info: 'fleet-synopsis',
|
|
category: 'Synopsis',
|
|
recordSchemas: ['SYN.AGENT.UPDATE_COMPLETE', 'SYN.AGENT.DELETE'],
|
|
},
|
|
{
|
|
path: 'how-to/create-update-delete-agent.md',
|
|
block: 4,
|
|
info: 'json',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.JSON.PARTIAL_FAILURE'],
|
|
},
|
|
{
|
|
path: 'how-to/customize-roles.md',
|
|
block: 1,
|
|
info: 'markdown',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.MARKDOWN.ROLE_TEMPLATE'],
|
|
},
|
|
{
|
|
path: 'how-to/customize-roles.md',
|
|
block: 2,
|
|
info: 'markdown',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.MARKDOWN.ROLE_EXAMPLE'],
|
|
},
|
|
{
|
|
path: 'how-to/start-stop-restart.md',
|
|
block: 1,
|
|
info: 'fleet-synopsis',
|
|
category: 'Synopsis',
|
|
recordSchemas: [
|
|
'SYN.APPLY.DRY',
|
|
'SYN.APPLY',
|
|
'SYN.RECONCILE',
|
|
'SYN.START.REQUIRED',
|
|
'SYN.STOP.REQUIRED',
|
|
'SYN.RESTART.REQUIRED',
|
|
'SYN.STATUS.OPTIONAL',
|
|
'SYN.VERIFY',
|
|
'SYN.DOCTOR',
|
|
],
|
|
},
|
|
{
|
|
path: 'migration/example-profile-disposition.md',
|
|
block: 1,
|
|
info: 'fleet-command',
|
|
category: 'ConcreteCommand',
|
|
recordSchemas: ['CMD.PNPM.MIGRATION_TEST'],
|
|
},
|
|
{
|
|
path: 'migration/v1-to-v2.md',
|
|
block: 1,
|
|
info: 'fleet-command',
|
|
category: 'ConcreteCommand',
|
|
recordSchemas: ['CMD.MIGRATE.PREVIEW'],
|
|
},
|
|
{
|
|
path: 'migration/v1-to-v2.md',
|
|
block: 2,
|
|
info: 'json',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.JSON.LIFECYCLE_OBSERVATIONS'],
|
|
},
|
|
{
|
|
path: 'operations/reconcile-and-recover.md',
|
|
block: 1,
|
|
info: 'json',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.JSON.RECOVERY_RESULT'],
|
|
},
|
|
{
|
|
path: 'reference/agent-mutations.md',
|
|
block: 1,
|
|
info: 'fleet-synopsis',
|
|
category: 'Synopsis',
|
|
recordSchemas: [
|
|
'SYN.AGENT.GET',
|
|
'SYN.PLAN.GENERIC',
|
|
'SYN.AGENT.CREATE',
|
|
'SYN.AGENT.UPDATE',
|
|
'SYN.AGENT.DELETE_DRY',
|
|
],
|
|
},
|
|
{
|
|
path: 'reference/agent-mutations.md',
|
|
block: 2,
|
|
info: 'json',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.JSON.MUTATION_RESULT'],
|
|
},
|
|
{
|
|
path: 'reference/cli.md',
|
|
block: 1,
|
|
info: 'fleet-synopsis',
|
|
category: 'Synopsis',
|
|
recordSchemas: [
|
|
'SYN.APPLY',
|
|
'SYN.RECONCILE',
|
|
'SYN.START.OPTIONAL',
|
|
'SYN.STOP.OPTIONAL',
|
|
'SYN.RESTART.OPTIONAL',
|
|
'SYN.STATUS.OPTIONAL',
|
|
'SYN.VERIFY',
|
|
'SYN.DOCTOR',
|
|
'SYN.MIGRATE.PREVIEW',
|
|
],
|
|
},
|
|
{
|
|
path: 'reference/generated-env-boundary.md',
|
|
block: 1,
|
|
info: 'dotenv',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.DOTENV.GENERATED_ENV'],
|
|
},
|
|
{
|
|
path: 'reference/roster-v2-fields.md',
|
|
block: 1,
|
|
info: 'yaml',
|
|
category: 'DataProfile',
|
|
recordSchemas: ['DATA.YAML.ROSTER_FIELDS'],
|
|
},
|
|
] as const satisfies readonly FenceProfile[];
|
|
|
|
const COMMAND_RECORDS: Readonly<Record<string, RegExp>> = {
|
|
'CMD.BACKLOG.CREATE.1': /^mosaic fleet backlog create --id A1 --title "schema" --priority 5$/,
|
|
'CMD.BACKLOG.CREATE.2':
|
|
/^mosaic fleet backlog create --id A2 --title "service" --depends-on A1 --priority 9$/,
|
|
'CMD.BACKLOG.CLAIM': /^mosaic fleet backlog claim --owner worker-1 --ttl 600 --json$/,
|
|
'CMD.BACKLOG.COMPLETE': /^mosaic fleet backlog complete --id A1$/,
|
|
'CMD.BACKLOG.LIST_READY': /^mosaic fleet backlog list --ready-only --json$/,
|
|
'CMD.BACKLOG.RECLAIM': /^mosaic fleet backlog reclaim --json$/,
|
|
'CMD.AGENT.CREATE_JSON':
|
|
/^mosaic fleet create --expected-generation [1-9][0-9]* --agent '\{\n(?:[ -~]*\n)*\}'$/,
|
|
'CMD.MIGRATE.PREVIEW':
|
|
/^mosaic fleet migrate-v1 preview \\\n --source [A-Za-z0-9_./@:+,=-]+ \\\n --decisions [A-Za-z0-9_./@:+,=-]+ \\\n --observations [A-Za-z0-9_./@:+,=-]+$/,
|
|
'CMD.PNPM.MIGRATION_TEST':
|
|
/^pnpm --filter @mosaicstack\/mosaic test -- v1-v2-migration\.spec\.ts \\\n -t "[A-Za-z0-9 _./@:+,=-]+"$/,
|
|
};
|
|
|
|
const DATA_PROFILE_BODIES: Readonly<Record<string, string>> = {
|
|
'DATA.DOTENV.FLEET_LAUNCH':
|
|
'MOSAIC_AGENT_NAME=<roster name>\nMOSAIC_AGENT_CLASS=<roster class>\nMOSAIC_AGENT_RUNTIME=<roster runtime>\nMOSAIC_AGENT_MODEL=<roster model hint>\nMOSAIC_AGENT_REASONING=<roster reasoning>\nMOSAIC_AGENT_TOOL_POLICY=<roster tool policy>\nMOSAIC_AGENT_WORKDIR=<absolute roster work directory>\nMOSAIC_TMUX_SOCKET=<roster socket or empty>',
|
|
'DATA.TEXT_TABLE.FLEET_TASKS':
|
|
'| W-FLEET | in-progress | Fleet (agent-session execution layer) | Phase 2/5 | docs/fleet/TASKS.md | observability dogfooded on live stub fleet; control plane rides federation (W1) |',
|
|
'DATA.TEXT_DIAGRAM.BACKLOG_FLOW':
|
|
' create\n \u2502\n \u25bc\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25ba ready \u2500\u2500\u2500\u2500\u2500 claim \u2500\u2500\u2500\u2500\u2500\u25ba claimed \u2500\u2500\u2500\u2500\u2500 complete \u2500\u2500\u2500\u2500\u2500\u25ba done\n \u2502 \u2502 \u2502\n \u2502 block reclaim (TTL expiry or --id)\n \u2502 \u25bc \u2502\n \u2502 blocked \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 (back to ready)\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 (reclaim / re-create can return a card to ready)',
|
|
'DATA.TYPESCRIPT.MATRIX_CONNECTOR_TYPE':
|
|
"interface OrchestratorConnector {\n readonly kind: 'tmux' | 'discord' | 'matrix';\n send(message: OutboundMessage): Promise<SendResult>; // orchestrator \u2192 human\n subscribe(handler: (m: InboundMessage) => void): Unsubscribe; // human \u2192 orchestrator\n health(): Promise<ConnectorHealth>; // reachable + authenticated\n}",
|
|
'DATA.YAML.MATRIX_CONNECTOR_CONFIG':
|
|
"connector:\n kind: matrix\n matrix:\n homeserver_url: https://matrix.example.internal\n user_id: '@mos:example.internal'\n room_id: '!abc:example.internal'",
|
|
'DATA.YAML.INTERACTION_AGENT':
|
|
'name: interaction-example\nalias: Interaction Example\nclass: interaction\ntool_policy: interaction\nlifecycle:\n enabled: true\n desired_state: stopped',
|
|
'DATA.YAML.VALIDATOR_AGENT':
|
|
'name: validator-example\nalias: Validator Example\nclass: validator\ntool_policy: validator\nlifecycle:\n enabled: true\n desired_state: stopped',
|
|
'DATA.JSON.PARTIAL_FAILURE':
|
|
'{\n "applied": false,\n "authoritativeRoster": "committed",\n "projections": "incomplete",\n "recovery": {\n "code": "projection-apply-failed",\n "action": "regenerate-projections-from-roster"\n }\n}',
|
|
'DATA.MARKDOWN.ROLE_TEMPLATE':
|
|
"# Code \u2014 local role definition\n\nThe local code role (`class: code`) follows the operator's repository conventions.",
|
|
'DATA.MARKDOWN.ROLE_EXAMPLE':
|
|
'# Release notes \u2014 local role definition\n\nThe release-notes role (`class: release-notes`) prepares operator-reviewed release copy.',
|
|
'DATA.JSON.LIFECYCLE_OBSERVATIONS':
|
|
'{\n "coder0": { "systemd": "inactive", "tmux": "missing" }\n}',
|
|
'DATA.JSON.RECOVERY_RESULT':
|
|
'{\n "applied": false,\n "authoritativeRoster": "unchanged",\n "projections": "incomplete",\n "lifecycle": "not-applied",\n "recovery": { "code": "projection-apply-failed", "action": "regenerate-projections-from-roster" }\n}',
|
|
'DATA.JSON.MUTATION_RESULT':
|
|
'{\n "applied": false,\n "authoritativeRoster": "committed",\n "projections": "incomplete",\n "recovery": {\n "code": "projection-apply-failed",\n "action": "regenerate-projections-from-roster"\n }\n}',
|
|
'DATA.DOTENV.GENERATED_ENV':
|
|
'MOSAIC_AGENT_NAME=<roster name>\nMOSAIC_AGENT_CLASS=<roster class>\nMOSAIC_AGENT_RUNTIME=<roster runtime>\nMOSAIC_AGENT_MODEL=<roster model hint>\nMOSAIC_AGENT_REASONING=<roster reasoning>\nMOSAIC_AGENT_TOOL_POLICY=<roster tool policy>\nMOSAIC_AGENT_WORKDIR=<absolute roster work directory>\nMOSAIC_TMUX_SOCKET=<roster socket or empty>',
|
|
'DATA.YAML.ROSTER_FIELDS':
|
|
'version: 2\ngeneration: 1\ntransport: tmux\ntmux:\n socket_name: mosaic-fleet\n holder_session: _holder\ndefaults:\n working_directory: ~/src\n runtime: pi\nruntimes:\n pi:\n reset_command: /new\nagents:\n - name: coder0\n alias: Coder 0\n class: code\n runtime: pi\n provider: openai\n model: gpt-5.6-sol\n reasoning: high\n tool_policy: code\n working_directory: ~/src\n persistent_persona: false\n reset_between_tasks: true\n lifecycle:\n enabled: true\n desired_state: stopped\n launch:\n yolo: true',
|
|
};
|
|
|
|
const SYNOPSIS_RECORDS: Readonly<Record<string, string>> = {
|
|
'SYN.AGENT.GET': 'mosaic fleet get <name>',
|
|
'SYN.PLAN.CREATE': "mosaic fleet plan create --expected-generation <n> --agent '<json>'",
|
|
'SYN.PLAN.UPDATE': "mosaic fleet plan update <name> --expected-generation <n> --agent '<json>'",
|
|
'SYN.PLAN.DELETE': 'mosaic fleet plan delete <name> --expected-generation <n>',
|
|
'SYN.AGENT.UPDATE_COMPLETE':
|
|
"mosaic fleet update <name> --expected-generation <n> --agent '<complete JSON agent payload>'",
|
|
'SYN.AGENT.DELETE': 'mosaic fleet delete <name> --expected-generation <n>',
|
|
'SYN.PLAN.GENERIC':
|
|
"mosaic fleet plan <create|update|delete> [<name>] --expected-generation <n> [--agent '<json>'] [--persisted-start]",
|
|
'SYN.AGENT.CREATE':
|
|
"mosaic fleet create --expected-generation <n> --agent '<json>' [--dry-run] [--persisted-start]",
|
|
'SYN.AGENT.UPDATE':
|
|
"mosaic fleet update <name> --expected-generation <n> --agent '<json>' [--dry-run]",
|
|
'SYN.AGENT.DELETE_DRY': 'mosaic fleet delete <name> --expected-generation <n> [--dry-run]',
|
|
'SYN.APPLY.DRY': 'mosaic fleet apply --expected-generation <n> --dry-run',
|
|
'SYN.APPLY': 'mosaic fleet apply --expected-generation <n>',
|
|
'SYN.RECONCILE': 'mosaic fleet reconcile --expected-generation <n>',
|
|
'SYN.START.REQUIRED': 'mosaic fleet start <name> --expected-generation <n>',
|
|
'SYN.STOP.REQUIRED': 'mosaic fleet stop <name> --expected-generation <n>',
|
|
'SYN.RESTART.REQUIRED': 'mosaic fleet restart <name> --expected-generation <n>',
|
|
'SYN.START.OPTIONAL': 'mosaic fleet start [<name>] --expected-generation <n> [--dry-run]',
|
|
'SYN.STOP.OPTIONAL': 'mosaic fleet stop [<name>] --expected-generation <n> [--dry-run]',
|
|
'SYN.RESTART.OPTIONAL': 'mosaic fleet restart [<name>] --expected-generation <n> [--dry-run]',
|
|
'SYN.STATUS.OPTIONAL': 'mosaic fleet status [<name>]',
|
|
'SYN.VERIFY': 'mosaic fleet verify',
|
|
'SYN.DOCTOR': 'mosaic fleet doctor',
|
|
'SYN.MIGRATE.PREVIEW':
|
|
'mosaic fleet migrate-v1 preview --source <path> --decisions <path> --observations <path>',
|
|
};
|
|
|
|
function profileFor(path: string, block: number): FenceProfile | undefined {
|
|
return FENCE_PROFILES.find(
|
|
(profile): boolean => profile.path === path && profile.block === block,
|
|
);
|
|
}
|
|
|
|
function publicDiagnostic(
|
|
surface: Pick<CodeSurface, 'path' | 'line' | 'block' | 'category'>,
|
|
code: string,
|
|
): SurfaceDiagnostic {
|
|
return {
|
|
path: surface.path,
|
|
line: surface.line,
|
|
...(surface.block === undefined ? {} : { block: surface.block }),
|
|
...(surface.category === undefined ? {} : { category: surface.category }),
|
|
code,
|
|
};
|
|
}
|
|
|
|
function codeSurfaceDiagnostics(path: string, source: string): SurfaceDiagnostic[] {
|
|
const diagnostics: SurfaceDiagnostic[] = [];
|
|
let inFence = false;
|
|
let block = 0;
|
|
|
|
for (const [index, line] of source.split('\n').entries()) {
|
|
const lineNumber = index + 1;
|
|
if (inFence) {
|
|
if (line === '```') inFence = false;
|
|
else if (/^(?:`{3,}|~{3,})/.test(line)) {
|
|
diagnostics.push({ path, line: lineNumber, block, code: 'fence-conflict' });
|
|
}
|
|
continue;
|
|
}
|
|
if (/^(?: {0,3}(?:(?:> ?)|(?:(?:[-+*]|[0-9]{1,9}[.)]) +)))+(`{3,}|~{3,})/.test(line)) {
|
|
diagnostics.push({ path, line: lineNumber, code: 'fence-context' });
|
|
continue;
|
|
}
|
|
if (/^(?: {4,}|\t).*\S/.test(line)) {
|
|
diagnostics.push({ path, line: lineNumber, code: 'indented-code' });
|
|
continue;
|
|
}
|
|
const marker = line.match(/^(`{3,}|~{3,})(.*)$/);
|
|
if (marker !== null) {
|
|
block += 1;
|
|
const info = marker[2] ?? '';
|
|
if (marker[1] !== '```' || /[`~]/.test(info)) {
|
|
diagnostics.push({ path, line: lineNumber, block, code: 'fence-marker' });
|
|
continue;
|
|
}
|
|
if (info === '' || !FENCE_PROFILES.some((profile): boolean => profile.info === info)) {
|
|
diagnostics.push({ path, line: lineNumber, block, code: 'fence-info' });
|
|
continue;
|
|
}
|
|
const profile = profileFor(path, block);
|
|
if (profile === undefined || profile.info !== info) {
|
|
diagnostics.push({ path, line: lineNumber, block, code: 'profile-unknown' });
|
|
continue;
|
|
}
|
|
inFence = true;
|
|
continue;
|
|
}
|
|
if (/`{2,}/.test(line)) {
|
|
diagnostics.push({ path, line: lineNumber, code: 'inline-delimiter' });
|
|
continue;
|
|
}
|
|
const withoutClosedInlineSpans = line.replace(/`[^`\n]+`/g, '');
|
|
if (withoutClosedInlineSpans.includes('`')) {
|
|
diagnostics.push({ path, line: lineNumber, code: 'inline-delimiter' });
|
|
continue;
|
|
}
|
|
if (/<\/?(?:pre|code|script|style|xmp|listing)(?=$|\s|[>/])/i.test(line)) {
|
|
diagnostics.push({ path, line: lineNumber, code: 'raw-code-container' });
|
|
}
|
|
for (const match of line.matchAll(/(?<!`)`([^`\n]+)`(?!`)/g)) {
|
|
if (!/^[A-Za-z0-9_./@:+,=-]{1,256}$/.test(match[1] ?? '')) {
|
|
diagnostics.push({
|
|
path,
|
|
line: lineNumber,
|
|
category: 'InlineLiteral',
|
|
code: 'inline-literal',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (inFence)
|
|
diagnostics.push({ path, line: source.split('\n').length, block, code: 'fence-unclosed' });
|
|
return diagnostics;
|
|
}
|
|
|
|
function codeSurfaces(path: string, source: string): CodeSurface[] {
|
|
const diagnostics = codeSurfaceDiagnostics(path, source);
|
|
if (diagnostics.length > 0) return [];
|
|
|
|
const surfaces: CodeSurface[] = [];
|
|
const lines = source.split('\n');
|
|
let fence:
|
|
| {
|
|
readonly block: number;
|
|
readonly line: number;
|
|
readonly info: string;
|
|
readonly body: string[];
|
|
}
|
|
| undefined;
|
|
let block = 0;
|
|
|
|
for (const [index, line] of lines.entries()) {
|
|
const lineNumber = index + 1;
|
|
if (fence !== undefined) {
|
|
if (line === '```') {
|
|
const profile = profileFor(path, fence.block);
|
|
if (profile !== undefined && profile.info === fence.info) {
|
|
surfaces.push({
|
|
category: profile.category,
|
|
path,
|
|
line: fence.line,
|
|
block: fence.block,
|
|
info: fence.info,
|
|
source: fence.body.join('\n'),
|
|
});
|
|
}
|
|
fence = undefined;
|
|
} else fence.body.push(line);
|
|
continue;
|
|
}
|
|
const opener = line.match(/^```([a-z-]+)$/);
|
|
if (opener !== null) {
|
|
block += 1;
|
|
fence = { block, line: lineNumber, info: opener[1] ?? '', body: [] };
|
|
continue;
|
|
}
|
|
for (const match of line.matchAll(/(?<!`)`([^`\n]+)`(?!`)/g)) {
|
|
surfaces.push({
|
|
category: 'InlineLiteral',
|
|
path,
|
|
line: lineNumber,
|
|
source: match[1] ?? '',
|
|
});
|
|
}
|
|
}
|
|
return surfaces;
|
|
}
|
|
|
|
function closedGrammarViolationKinds(surface: CodeSurface): string[] {
|
|
const kinds = new Set<string>();
|
|
const credentialFormat =
|
|
/(?:\bAKIA[0-9A-Z]{16}\b|\bAIza[0-9A-Za-z_-]{35}\b|\bgh[pousr]_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b|\bglpat-[A-Za-z0-9_-]{20,}\b|\bnpm_[A-Za-z0-9]{20,}\b|\bsk-ant-(?:api\d{2}-)?[A-Za-z0-9_-]{20,}\b|\bsk-proj-[A-Za-z0-9_-]{20,}\b|\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b|\bxox[baprs]-[A-Za-z0-9-]{10,}\b|\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b|\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b|-----BEGIN [A-Z ]*PRIVATE KEY-----|\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s/:]+:[^\s/@]+@)/;
|
|
|
|
if (credentialFormat.test(surface.source)) kinds.add('credential-format');
|
|
if (/\b(?:Tess|Ultron)\b/.test(surface.source)) kinds.add('identity');
|
|
|
|
if (surface.category === 'InlineLiteral') {
|
|
if (!/^[A-Za-z0-9_./@:+,=-]{1,256}$/.test(surface.source)) kinds.add('inline-literal');
|
|
return [...kinds].sort();
|
|
}
|
|
|
|
const profile = profileFor(surface.path, surface.block ?? 0);
|
|
if (
|
|
profile === undefined ||
|
|
profile.info !== surface.info ||
|
|
profile.category !== surface.category
|
|
) {
|
|
kinds.add('profile-unknown');
|
|
return [...kinds].sort();
|
|
}
|
|
|
|
if (surface.category === 'DataProfile') {
|
|
const schema = profile.recordSchemas?.[0] ?? '';
|
|
if (profile.recordSchemas?.length !== 1 || surface.source !== DATA_PROFILE_BODIES[schema]) {
|
|
kinds.add('data-profile');
|
|
}
|
|
return [...kinds].sort();
|
|
}
|
|
if (/^(?:# |\$ |> )/m.test(surface.source)) kinds.add('comment-or-prompt');
|
|
if (/(?:^|\s)[A-Za-z_][A-Za-z0-9_]*\+?=[^\s]*/.test(surface.source)) kinds.add('assignment');
|
|
|
|
const schemas = profile.recordSchemas ?? [];
|
|
if (surface.category === 'Synopsis') {
|
|
const records = surface.source.split('\n').filter((record): boolean => record !== '');
|
|
if (
|
|
records.length !== schemas.length ||
|
|
records.some((record, index): boolean => record !== SYNOPSIS_RECORDS[schemas[index] ?? ''])
|
|
) {
|
|
kinds.add('synopsis-schema');
|
|
}
|
|
} else if (schemas.length === 1) {
|
|
if (!COMMAND_RECORDS[schemas[0] ?? '']?.test(surface.source)) kinds.add('command-schema');
|
|
} else {
|
|
const records = surface.source.split('\n').filter((record): boolean => record !== '');
|
|
if (
|
|
records.length !== schemas.length ||
|
|
records.some((record, index): boolean => !COMMAND_RECORDS[schemas[index] ?? '']?.test(record))
|
|
) {
|
|
kinds.add('command-schema');
|
|
}
|
|
}
|
|
return [...kinds].sort();
|
|
}
|
|
|
|
function surfaceDiagnostics(surfaces: readonly CodeSurface[]): SurfaceDiagnostic[] {
|
|
return surfaces.flatMap((surface): SurfaceDiagnostic[] =>
|
|
closedGrammarViolationKinds(surface).map(
|
|
(kind): SurfaceDiagnostic => publicDiagnostic(surface, kind),
|
|
),
|
|
);
|
|
}
|
|
|
|
const CLOSED_GRAMMAR_REJECTION_FIXTURES = [
|
|
'sudo systemctl restart example',
|
|
'env -S apt-get install example',
|
|
"sh -c 'apt-get --version'",
|
|
'su root',
|
|
'command -- apt-get install example',
|
|
'mosaic fleet verify; reboot',
|
|
'# mosaic fleet verify',
|
|
'$ mosaic fleet verify',
|
|
'FOO=value mosaic fleet verify',
|
|
] as const;
|
|
|
|
describe('closed documentation publication grammar', (): void => {
|
|
it('classifies only the four approved code-surface categories', (): void => {
|
|
const categories: readonly CodeSurfaceCategory[] = [
|
|
'ConcreteCommand',
|
|
'Synopsis',
|
|
'DataProfile',
|
|
'InlineLiteral',
|
|
];
|
|
expect(new Set(categories)).toEqual(
|
|
new Set(['ConcreteCommand', 'Synopsis', 'DataProfile', 'InlineLiteral']),
|
|
);
|
|
});
|
|
|
|
it.each(CLOSED_GRAMMAR_REJECTION_FIXTURES)(
|
|
'rejects shell-shaped input without parsing shell grammar',
|
|
(source): void => {
|
|
const surface: CodeSurface = {
|
|
category: 'ConcreteCommand',
|
|
path: 'migration/v1-to-v2.md',
|
|
line: 1,
|
|
block: 1,
|
|
info: 'fleet-command',
|
|
source,
|
|
};
|
|
expect(closedGrammarViolationKinds(surface)).toContain('command-schema');
|
|
},
|
|
);
|
|
|
|
it('rejects DSL comments, prompt prefixes, assignments, and root-prompt ambiguity', (): void => {
|
|
for (const source of [
|
|
'# mosaic fleet verify',
|
|
'$ mosaic fleet verify',
|
|
'> mosaic fleet verify',
|
|
'ROOT=1 mosaic fleet verify',
|
|
]) {
|
|
const surface: CodeSurface = {
|
|
category: 'Synopsis',
|
|
path: 'reference/cli.md',
|
|
line: 1,
|
|
block: 1,
|
|
info: 'fleet-synopsis',
|
|
source,
|
|
};
|
|
expect(closedGrammarViolationKinds(surface)).not.toEqual([]);
|
|
}
|
|
});
|
|
|
|
it('rejects unmatched single-backtick delimiters on either side', (): void => {
|
|
for (const source of ['`literal', 'literal`', 'text `literal', 'literal` text']) {
|
|
expect(codeSurfaceDiagnostics('fixture.md', source)).toContainEqual({
|
|
path: 'fixture.md',
|
|
line: 1,
|
|
code: 'inline-delimiter',
|
|
});
|
|
}
|
|
});
|
|
|
|
it('counts every invalid column-one fence candidate before later profile selection', (): void => {
|
|
for (const firstCandidate of [
|
|
'````fleet-synopsis',
|
|
'```fleet-synopsis```',
|
|
'~~~fleet-synopsis~~~',
|
|
]) {
|
|
expect(
|
|
codeSurfaceDiagnostics(
|
|
'reference/cli.md',
|
|
`${firstCandidate}\n\`\`\`unknown\n\`\`\`fleet-synopsis\nmosaic fleet verify\n\`\`\``,
|
|
),
|
|
).toEqual([
|
|
{ path: 'reference/cli.md', line: 1, block: 1, code: 'fence-marker' },
|
|
{ path: 'reference/cli.md', line: 2, block: 2, code: 'fence-info' },
|
|
{ path: 'reference/cli.md', line: 3, block: 3, code: 'profile-unknown' },
|
|
{ path: 'reference/cli.md', line: 5, block: 4, code: 'fence-info' },
|
|
]);
|
|
}
|
|
});
|
|
|
|
it('rejects inline delimiter runs instead of silently omitting them', (): void => {
|
|
for (const source of ['``literal``', 'text ```literal```', 'text ``literal`` text']) {
|
|
expect(codeSurfaceDiagnostics('fixture.md', source)).toContainEqual({
|
|
path: 'fixture.md',
|
|
line: 1,
|
|
code: 'inline-delimiter',
|
|
});
|
|
}
|
|
});
|
|
|
|
it('rejects every nonblank line with four leading spaces or a leading tab', (): void => {
|
|
for (const source of [
|
|
' mosaic fleet verify',
|
|
' mosaic fleet verify',
|
|
' \tmosaic fleet verify',
|
|
'\t mosaic fleet verify',
|
|
]) {
|
|
expect(codeSurfaceDiagnostics('fixture.md', source)).toEqual([
|
|
{ path: 'fixture.md', line: 1, code: 'indented-code' },
|
|
]);
|
|
}
|
|
});
|
|
|
|
it('rejects raw HTML code-container tag prefixes at every boundary', (): void => {
|
|
for (const source of [
|
|
'<pre',
|
|
'<pre ',
|
|
'<pre>',
|
|
'<pre/',
|
|
'<code',
|
|
'<code ',
|
|
'<code>',
|
|
'<code/',
|
|
]) {
|
|
expect(codeSurfaceDiagnostics('fixture.md', source)).toEqual([
|
|
{ path: 'fixture.md', line: 1, code: 'raw-code-container' },
|
|
]);
|
|
}
|
|
expect(codeSurfaceDiagnostics('fixture.md', '<prelude>prose</prelude>')).toEqual([]);
|
|
});
|
|
|
|
it('rejects nested blockquote and list fence contexts', (): void => {
|
|
for (const source of [
|
|
'>> ```fleet-command',
|
|
'> > ```fleet-command',
|
|
'> - ```fleet-command',
|
|
'- > ```fleet-command',
|
|
]) {
|
|
expect(codeSurfaceDiagnostics('fixture.md', source)).toEqual([
|
|
{ path: 'fixture.md', line: 1, code: 'fence-context' },
|
|
]);
|
|
}
|
|
});
|
|
|
|
it('requires optional metavariables to use bracketed angle notation', (): void => {
|
|
const accepted: CodeSurface = {
|
|
category: 'Synopsis',
|
|
path: 'reference/cli.md',
|
|
line: 1,
|
|
block: 1,
|
|
info: 'fleet-synopsis',
|
|
source: Object.values(SYNOPSIS_RECORDS)
|
|
.filter((record): boolean =>
|
|
[
|
|
'SYN.APPLY',
|
|
'SYN.RECONCILE',
|
|
'SYN.START.OPTIONAL',
|
|
'SYN.STOP.OPTIONAL',
|
|
'SYN.RESTART.OPTIONAL',
|
|
'SYN.STATUS.OPTIONAL',
|
|
'SYN.VERIFY',
|
|
'SYN.DOCTOR',
|
|
'SYN.MIGRATE.PREVIEW',
|
|
].includes(
|
|
Object.entries(SYNOPSIS_RECORDS).find(([, value]): boolean => value === record)?.[0] ??
|
|
'',
|
|
),
|
|
)
|
|
.join('\n'),
|
|
};
|
|
expect(closedGrammarViolationKinds(accepted)).toEqual([]);
|
|
expect(
|
|
closedGrammarViolationKinds({
|
|
...accepted,
|
|
source: accepted.source.replace('[<name>]', '[name]'),
|
|
}),
|
|
).toContain('synopsis-schema');
|
|
});
|
|
|
|
it('emits only location, category, and closed diagnostic codes', (): void => {
|
|
const sensitiveFixture = ['sk-ant-api03-', 'a'.repeat(80)].join('');
|
|
const diagnostic = surfaceDiagnostics([
|
|
{
|
|
category: 'InlineLiteral',
|
|
path: 'fixture.md',
|
|
line: 7,
|
|
source: sensitiveFixture,
|
|
},
|
|
]);
|
|
expect(diagnostic).toEqual([
|
|
{
|
|
path: 'fixture.md',
|
|
line: 7,
|
|
category: 'InlineLiteral',
|
|
code: 'credential-format',
|
|
},
|
|
]);
|
|
expect(JSON.stringify(diagnostic)).not.toContain(sensitiveFixture);
|
|
});
|
|
});
|
|
|
|
describe('fleet operator documentation', (): void => {
|
|
it('ships every accepted information-architecture page', async (): Promise<void> => {
|
|
await expect(
|
|
Promise.all(REQUIRED_FLEET_PAGES.map((path) => readFile(join(fleetDocs, path), 'utf8'))),
|
|
).resolves.toHaveLength(REQUIRED_FLEET_PAGES.length);
|
|
});
|
|
|
|
it('resolves every local Markdown link and heading fragment in the fleet book and sitemap', async (): Promise<void> => {
|
|
const files = [...(await markdownFiles(fleetDocs)), join(repositoryRoot, 'docs', 'SITEMAP.md')];
|
|
const documents: Record<string, string> = {};
|
|
for (const file of files) {
|
|
const relative = file.slice(repositoryRoot.length + 1);
|
|
documents[relative] = await readFile(file, 'utf8');
|
|
}
|
|
|
|
const violations: string[] = [];
|
|
for (const [sourcePath, source] of Object.entries(documents)) {
|
|
for (const target of localMarkdownTargets(source)) {
|
|
const encodedPath = target.split('#', 1)[0] ?? '';
|
|
const targetPath = resolve(
|
|
dirname(join(repositoryRoot, sourcePath)),
|
|
decodeURIComponent(encodedPath),
|
|
);
|
|
const relativeTarget = targetPath.slice(repositoryRoot.length + 1);
|
|
if (documents[relativeTarget] === undefined) {
|
|
try {
|
|
documents[relativeTarget] = await readFile(targetPath, 'utf8');
|
|
} catch {
|
|
// The deterministic validator below records the missing target without exposing content.
|
|
}
|
|
}
|
|
}
|
|
violations.push(...markdownLinkViolations(sourcePath, source, documents));
|
|
}
|
|
expect(violations).toEqual([]);
|
|
});
|
|
|
|
it('validates the canonical documentation example through the production compiler and resolver', async (): Promise<void> => {
|
|
const source = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8');
|
|
const roster = parseRosterV2(source, 'yaml');
|
|
const validated = await validateRosterV2Semantics(roster, {
|
|
rolesDir: join(frameworkFleet, 'roles'),
|
|
overrideDir: join(fleetDocs, 'examples', 'roles.local'),
|
|
});
|
|
|
|
expect(validated.generation).toBe(1);
|
|
expect(validated.agents.map((agent) => agent.canonicalClass)).toEqual([
|
|
'code',
|
|
'interaction',
|
|
'validator',
|
|
]);
|
|
});
|
|
|
|
it('keeps every rendered fleet code surface in one closed category without exposing sensitive values', async (): Promise<void> => {
|
|
const diagnostics: SurfaceDiagnostic[] = [];
|
|
const surfaces: CodeSurface[] = [];
|
|
for (const file of await markdownFiles(fleetDocs)) {
|
|
const source = await readFile(file, 'utf8');
|
|
const relative = file.slice(fleetDocs.length + 1);
|
|
diagnostics.push(...codeSurfaceDiagnostics(relative, source));
|
|
surfaces.push(...codeSurfaces(relative, source));
|
|
}
|
|
diagnostics.push(...surfaceDiagnostics(surfaces));
|
|
|
|
expect(diagnostics).toEqual([]);
|
|
expect(
|
|
surfaces.filter((surface): boolean => surface.category === 'ConcreteCommand'),
|
|
).toHaveLength(4);
|
|
expect(surfaces.filter((surface): boolean => surface.category === 'Synopsis')).toHaveLength(5);
|
|
expect(surfaces.filter((surface): boolean => surface.category === 'DataProfile')).toHaveLength(
|
|
15,
|
|
);
|
|
expect(
|
|
surfaces.filter((surface): boolean => surface.category === 'InlineLiteral'),
|
|
).toHaveLength(858);
|
|
expect(surfaces).toHaveLength(882);
|
|
|
|
const rosterSource = await readFile(join(fleetDocs, 'examples', 'roster-v2.yaml'), 'utf8');
|
|
const auxiliary: CodeSurface = {
|
|
category: 'DataProfile',
|
|
path: 'examples/roster-v2.yaml',
|
|
line: 1,
|
|
source: rosterSource,
|
|
};
|
|
expect(
|
|
closedGrammarViolationKinds(auxiliary).filter((kind): boolean => kind !== 'profile-unknown'),
|
|
).toEqual([]);
|
|
});
|
|
});
|