@urielsh/prodify 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prodify/AGENTS.md +56 -0
- package/.prodify/README.md +10 -0
- package/.prodify/artifacts/01-understand.md +28 -0
- package/.prodify/artifacts/02-diagnose.md +26 -0
- package/.prodify/artifacts/03-architecture.md +30 -0
- package/.prodify/artifacts/04-plan.md +35 -0
- package/.prodify/artifacts/05-refactor.md +24 -0
- package/.prodify/artifacts/06-validate.md +17 -0
- package/.prodify/artifacts/README.md +6 -0
- package/.prodify/artifacts/architecture_spec.md +29 -0
- package/.prodify/artifacts/artifact-validation-design.md +276 -0
- package/.prodify/artifacts/cli-command-design.md +299 -0
- package/.prodify/artifacts/completed-steps-tracking.md +201 -0
- package/.prodify/artifacts/diagnostic_report.md +45 -0
- package/.prodify/artifacts/hardening-patch-summary.md +57 -0
- package/.prodify/artifacts/hardening-verification-report.md +48 -0
- package/.prodify/artifacts/implementation_summary.md +19 -0
- package/.prodify/artifacts/improvement-evaluation-spec.md +148 -0
- package/.prodify/artifacts/next-step-resolver-design.md +570 -0
- package/.prodify/artifacts/orientation_map.md +32 -0
- package/.prodify/artifacts/persona-removal-audit.md +106 -0
- package/.prodify/artifacts/planning-alignment-report.md +189 -0
- package/.prodify/artifacts/refactor-plan-template-hardening.md +83 -0
- package/.prodify/artifacts/refactor-validate-loop-design.md +231 -0
- package/.prodify/artifacts/refactor_plan.md +21 -0
- package/.prodify/artifacts/refactor_plan.strict-example.md +31 -0
- package/.prodify/artifacts/run-state-design.md +292 -0
- package/.prodify/artifacts/run-summary-spec.md +149 -0
- package/.prodify/artifacts/run_state.json +14 -0
- package/.prodify/artifacts/sample-repo-test-plan.md +129 -0
- package/.prodify/artifacts/status-output-spec.md +349 -0
- package/.prodify/artifacts/step-selection-design.md +251 -0
- package/.prodify/artifacts/task-dispatcher-design.md +266 -0
- package/.prodify/artifacts/task-header-audit.md +42 -0
- package/.prodify/artifacts/task-log-enhancement-spec.md +264 -0
- package/.prodify/artifacts/task-protocol-hardening-plan.md +68 -0
- package/.prodify/artifacts/task-self-validation-spec.md +171 -0
- package/.prodify/artifacts/task_log.json +160 -0
- package/.prodify/artifacts/template-usage-audit.md +20 -0
- package/.prodify/artifacts/validation_report.md +22 -0
- package/.prodify/contracts/README.md +3 -0
- package/.prodify/contracts/architecture.contract.json +42 -0
- package/.prodify/contracts/diagnose.contract.json +42 -0
- package/.prodify/contracts/plan.contract.json +42 -0
- package/.prodify/contracts/refactor.contract.json +45 -0
- package/.prodify/contracts/understand.contract.json +42 -0
- package/.prodify/contracts/validate.contract.json +52 -0
- package/.prodify/contracts-src/README.md +4 -0
- package/.prodify/contracts-src/architecture.contract.md +29 -0
- package/.prodify/contracts-src/diagnose.contract.md +29 -0
- package/.prodify/contracts-src/plan.contract.md +29 -0
- package/.prodify/contracts-src/refactor.contract.md +32 -0
- package/.prodify/contracts-src/understand.contract.md +29 -0
- package/.prodify/contracts-src/validate.contract.md +35 -0
- package/.prodify/metrics/README.md +4 -0
- package/.prodify/planning.md +13 -0
- package/.prodify/project.md +15 -0
- package/.prodify/rules/01-global-operating-rules.md +21 -0
- package/.prodify/rules/02-analysis-discipline.md +17 -0
- package/.prodify/rules/03-diagnosis-severity-rules.md +42 -0
- package/.prodify/rules/04-architecture-rules.md +27 -0
- package/.prodify/rules/05-planning-rules.md +23 -0
- package/.prodify/rules/06-refactor-rules.md +20 -0
- package/.prodify/rules/07-validation-rules.md +25 -0
- package/.prodify/rules/08-output-format-rules.md +20 -0
- package/.prodify/rules/09-template-usage-rules.md +11 -0
- package/.prodify/rules/10-artifact-flow-and-orchestration-rules.md +40 -0
- package/.prodify/rules/README.md +3 -0
- package/.prodify/rules/example-rule.md +4 -0
- package/.prodify/runtime-commands.md +57 -0
- package/.prodify/skills/README.md +8 -0
- package/.prodify/skills/domain/react-frontend.skill.json +34 -0
- package/.prodify/skills/domain/typescript-backend.skill.json +34 -0
- package/.prodify/skills/quality-policy/maintainability-review.skill.json +25 -0
- package/.prodify/skills/quality-policy/security-hardening.skill.json +32 -0
- package/.prodify/skills/quality-policy/test-hardening.skill.json +23 -0
- package/.prodify/skills/registry.json +16 -0
- package/.prodify/skills/stage-method/architecture-method.skill.json +22 -0
- package/.prodify/skills/stage-method/codebase-scanning.skill.json +22 -0
- package/.prodify/skills/stage-method/diagnosis-method.skill.json +22 -0
- package/.prodify/skills/stage-method/planning-method.skill.json +22 -0
- package/.prodify/skills/stage-method/refactoring-method.skill.json +22 -0
- package/.prodify/skills/stage-method/validation-method.skill.json +22 -0
- package/.prodify/state.json +30 -0
- package/.prodify/tasks/01-understand.md +83 -0
- package/.prodify/tasks/02-diagnose.md +67 -0
- package/.prodify/tasks/03-architecture.md +70 -0
- package/.prodify/tasks/04-plan.md +71 -0
- package/.prodify/tasks/05-refactor.md +61 -0
- package/.prodify/tasks/06-validate.md +69 -0
- package/.prodify/tasks/README.md +3 -0
- package/.prodify/tasks/example-task.md +13 -0
- package/.prodify/templates/01-understand.template.md +18 -0
- package/.prodify/templates/02-diagnose.template.md +18 -0
- package/.prodify/templates/03-architecture.template.md +18 -0
- package/.prodify/templates/04-plan.template.md +22 -0
- package/.prodify/templates/05-refactor.template.md +19 -0
- package/.prodify/templates/06-validate.template.md +16 -0
- package/.prodify/templates/README.md +3 -0
- package/.prodify/templates/architecture_spec.template.md +29 -0
- package/.prodify/templates/diagnostic_report.template.md +45 -0
- package/.prodify/templates/example.template.md +5 -0
- package/.prodify/templates/implementation_summary.template.md +19 -0
- package/.prodify/templates/orientation_map.template.md +32 -0
- package/.prodify/templates/refactor_plan.template.md +24 -0
- package/.prodify/templates/validation_report.template.md +22 -0
- package/.prodify/version.json +5 -0
- package/AGENTS.md +305 -0
- package/LICENSE +201 -0
- package/README.md +118 -0
- package/assets/presets/default/canonical/AGENTS.md +54 -0
- package/assets/presets/default/canonical/artifacts/README.md +6 -0
- package/assets/presets/default/canonical/contracts-src/README.md +4 -0
- package/assets/presets/default/canonical/contracts-src/architecture.contract.md +56 -0
- package/assets/presets/default/canonical/contracts-src/diagnose.contract.md +49 -0
- package/assets/presets/default/canonical/contracts-src/plan.contract.md +42 -0
- package/assets/presets/default/canonical/contracts-src/refactor.contract.md +54 -0
- package/assets/presets/default/canonical/contracts-src/understand.contract.md +56 -0
- package/assets/presets/default/canonical/contracts-src/validate.contract.md +64 -0
- package/assets/presets/default/canonical/metrics/README.md +4 -0
- package/assets/presets/default/canonical/planning.md +13 -0
- package/assets/presets/default/canonical/project.md +15 -0
- package/assets/presets/default/canonical/rules/README.md +3 -0
- package/assets/presets/default/canonical/rules/example-rule.md +4 -0
- package/assets/presets/default/canonical/runtime-commands.md +57 -0
- package/assets/presets/default/canonical/skills/README.md +8 -0
- package/assets/presets/default/canonical/skills/domain/react-frontend.skill.json +34 -0
- package/assets/presets/default/canonical/skills/domain/typescript-backend.skill.json +34 -0
- package/assets/presets/default/canonical/skills/quality-policy/maintainability-review.skill.json +25 -0
- package/assets/presets/default/canonical/skills/quality-policy/security-hardening.skill.json +32 -0
- package/assets/presets/default/canonical/skills/quality-policy/test-hardening.skill.json +23 -0
- package/assets/presets/default/canonical/skills/registry.json +16 -0
- package/assets/presets/default/canonical/skills/stage-method/architecture-method.skill.json +22 -0
- package/assets/presets/default/canonical/skills/stage-method/codebase-scanning.skill.json +22 -0
- package/assets/presets/default/canonical/skills/stage-method/diagnosis-method.skill.json +22 -0
- package/assets/presets/default/canonical/skills/stage-method/planning-method.skill.json +22 -0
- package/assets/presets/default/canonical/skills/stage-method/refactoring-method.skill.json +22 -0
- package/assets/presets/default/canonical/skills/stage-method/validation-method.skill.json +22 -0
- package/assets/presets/default/canonical/state.json +30 -0
- package/assets/presets/default/canonical/tasks/README.md +3 -0
- package/assets/presets/default/canonical/tasks/example-task.md +13 -0
- package/assets/presets/default/canonical/templates/README.md +3 -0
- package/assets/presets/default/canonical/templates/example.template.md +5 -0
- package/assets/presets/default/preset.json +5 -0
- package/dist/cli.js +53 -0
- package/dist/commands/doctor.js +16 -0
- package/dist/commands/init.js +30 -0
- package/dist/commands/install.js +27 -0
- package/dist/commands/setup-agent.js +23 -0
- package/dist/commands/status.js +28 -0
- package/dist/commands/sync.js +29 -0
- package/dist/commands/update.js +18 -0
- package/dist/contracts/compiled-schema.js +37 -0
- package/dist/contracts/compiler.js +83 -0
- package/dist/contracts/freshness.js +52 -0
- package/dist/contracts/index.js +5 -0
- package/dist/contracts/parser.js +201 -0
- package/dist/contracts/schema.js +138 -0
- package/dist/contracts/source-schema.js +111 -0
- package/dist/core/agent-runtime.js +36 -0
- package/dist/core/agent-setup.js +111 -0
- package/dist/core/doctor.js +155 -0
- package/dist/core/errors.js +19 -0
- package/dist/core/flow-state.js +262 -0
- package/dist/core/fs.js +50 -0
- package/dist/core/install.js +44 -0
- package/dist/core/managed-files.js +106 -0
- package/dist/core/paths.js +56 -0
- package/dist/core/preset-validation.js +43 -0
- package/dist/core/prompt-builder.js +63 -0
- package/dist/core/repo-context.js +77 -0
- package/dist/core/repo-root.js +55 -0
- package/dist/core/skill-resolution.js +123 -0
- package/dist/core/state.js +220 -0
- package/dist/core/status.js +293 -0
- package/dist/core/sync.js +63 -0
- package/dist/core/targets.js +48 -0
- package/dist/core/upgrade.js +57 -0
- package/dist/core/validation.js +138 -0
- package/dist/core/version-checks.js +35 -0
- package/dist/generators/claude.js +14 -0
- package/dist/generators/codex.js +14 -0
- package/dist/generators/copilot.js +29 -0
- package/dist/generators/header.js +13 -0
- package/dist/generators/opencode.js +14 -0
- package/dist/generators/shared.js +37 -0
- package/dist/index.js +9 -0
- package/dist/legacy/targets.js +55 -0
- package/dist/presets/default.js +3 -0
- package/dist/presets/loader.js +34 -0
- package/dist/presets/version.js +18 -0
- package/dist/scoring/model.js +262 -0
- package/dist/skills/loader.js +40 -0
- package/dist/skills/schema.js +159 -0
- package/dist/types.js +1 -0
- package/docs/canonical-prodify-layout.md +87 -0
- package/docs/claude-support.md +22 -0
- package/docs/cli-doctor-spec.md +52 -0
- package/docs/cli-init-spec.md +70 -0
- package/docs/cli-install-agent-spec.md +48 -0
- package/docs/cli-sync-spec.md +40 -0
- package/docs/codex-support.md +24 -0
- package/docs/compatibility-targets.md +59 -0
- package/docs/copilot-support.md +30 -0
- package/docs/default-preset.md +47 -0
- package/docs/generated-file-headers.md +45 -0
- package/docs/generation-rules.md +94 -0
- package/docs/idempotency-guarantees.md +31 -0
- package/docs/managed-file-detection.md +45 -0
- package/docs/manual-edit-conflicts.md +37 -0
- package/docs/opencode-support.md +27 -0
- package/docs/ownership-rules.md +39 -0
- package/docs/path-resolution-rules.md +40 -0
- package/docs/preset-structure.md +49 -0
- package/docs/skill-system.md +67 -0
- package/docs/versioning-and-upgrade-strategy.md +40 -0
- package/package.json +22 -0
- package/src/README.md +11 -0
- package/src/cli.ts +61 -0
- package/src/commands/doctor.ts +20 -0
- package/src/commands/init.ts +37 -0
- package/src/commands/setup-agent.ts +28 -0
- package/src/commands/status.ts +34 -0
- package/src/commands/update.ts +23 -0
- package/src/contracts/README.md +10 -0
- package/src/contracts/compiled-schema.ts +42 -0
- package/src/contracts/compiler.ts +111 -0
- package/src/contracts/freshness.ts +58 -0
- package/src/contracts/index.ts +11 -0
- package/src/contracts/parser.ts +253 -0
- package/src/contracts/source-schema.ts +141 -0
- package/src/core/agent-runtime.ts +53 -0
- package/src/core/agent-setup.ts +147 -0
- package/src/core/doctor.ts +171 -0
- package/src/core/errors.ts +28 -0
- package/src/core/flow-state.ts +333 -0
- package/src/core/fs.ts +59 -0
- package/src/core/paths.ts +63 -0
- package/src/core/preset-validation.ts +47 -0
- package/src/core/prompt-builder.ts +73 -0
- package/src/core/repo-context.ts +93 -0
- package/src/core/repo-root.ts +74 -0
- package/src/core/skill-resolution.ts +151 -0
- package/src/core/state.ts +264 -0
- package/src/core/status.ts +372 -0
- package/src/core/targets.ts +53 -0
- package/src/core/upgrade.ts +66 -0
- package/src/core/validation.ts +233 -0
- package/src/core/version-checks.ts +40 -0
- package/src/index.ts +13 -0
- package/src/presets/default.ts +7 -0
- package/src/presets/loader.ts +46 -0
- package/src/presets/version.ts +31 -0
- package/src/scoring/model.ts +332 -0
- package/src/skills/loader.ts +58 -0
- package/src/skills/schema.ts +197 -0
- package/src/types.ts +329 -0
- package/tests/integration/cli-flows.test.js +347 -0
- package/tests/unit/agent-setup.test.js +81 -0
- package/tests/unit/cli.test.js +28 -0
- package/tests/unit/contracts.test.js +61 -0
- package/tests/unit/helpers.js +28 -0
- package/tests/unit/paths.test.js +52 -0
- package/tests/unit/preset-loader.test.js +37 -0
- package/tests/unit/scoring.test.js +65 -0
- package/tests/unit/self-hosted-workspace.test.js +22 -0
- package/tests/unit/skills.test.js +115 -0
- package/tests/unit/state-and-flow.test.js +120 -0
- package/tests/unit/targets.test.js +13 -0
- package/tests/unit/validation.test.js +73 -0
- package/tests/unit/version.test.js +24 -0
- package/tsconfig.json +23 -0
- package/urielsh-prodify-0.1.0.tgz +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { setupAgentIntegration } from '../core/agent-setup.js';
|
|
2
|
+
import { ProdifyError } from '../core/errors.js';
|
|
3
|
+
import type { CommandContext } from '../types.js';
|
|
4
|
+
|
|
5
|
+
function parseAgent(args: string[]): string {
|
|
6
|
+
const agent = args[0] ?? null;
|
|
7
|
+
if (!agent || args.length !== 1) {
|
|
8
|
+
throw new ProdifyError('setup-agent requires exactly one argument: <codex|claude|copilot|opencode>.', {
|
|
9
|
+
code: 'INVALID_AGENT'
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return agent;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function runSetupAgentCommand(args: string[], context: CommandContext): Promise<number> {
|
|
17
|
+
const agent = parseAgent(args);
|
|
18
|
+
const result = await setupAgentIntegration(agent);
|
|
19
|
+
|
|
20
|
+
context.stdout.write('Prodify Agent Setup\n');
|
|
21
|
+
context.stdout.write(`Agent: ${agent}\n`);
|
|
22
|
+
context.stdout.write(`Status: ${result.alreadyConfigured ? 'already configured globally; refreshed' : 'configured globally'}\n`);
|
|
23
|
+
context.stdout.write(`Configured agents: ${result.configuredAgents.join(', ')}\n`);
|
|
24
|
+
context.stdout.write(`Registry: ${result.statePath}\n`);
|
|
25
|
+
context.stdout.write('Repo impact: none\n');
|
|
26
|
+
context.stdout.write('Next step: run `prodify init` in a repository, then open that agent and use `$prodify-init`.\n');
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { resolveRepoRoot } from '../core/repo-root.js';
|
|
2
|
+
import { inspectRepositoryStatus, renderStatusReport } from '../core/status.js';
|
|
3
|
+
import { ProdifyError } from '../core/errors.js';
|
|
4
|
+
import { getRuntimeProfile } from '../core/targets.js';
|
|
5
|
+
import type { CommandContext } from '../types.js';
|
|
6
|
+
|
|
7
|
+
function parseRequestedAgent(args: string[]): string | null {
|
|
8
|
+
const agentFlagIndex = args.indexOf('--agent');
|
|
9
|
+
if (agentFlagIndex === -1) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const value = args[agentFlagIndex + 1] ?? null;
|
|
14
|
+
if (!value || !getRuntimeProfile(value)) {
|
|
15
|
+
throw new ProdifyError('status requires --agent <codex|claude|copilot|opencode> when an agent is specified.', {
|
|
16
|
+
code: 'INVALID_AGENT'
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runStatusCommand(args: string[], context: CommandContext): Promise<number> {
|
|
24
|
+
const repoRoot = await resolveRepoRoot({
|
|
25
|
+
cwd: context.cwd,
|
|
26
|
+
allowBootstrap: true
|
|
27
|
+
});
|
|
28
|
+
const report = await inspectRepositoryStatus(repoRoot, {
|
|
29
|
+
agent: parseRequestedAgent(args)
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
context.stdout.write(`${renderStatusReport(report)}\n`);
|
|
33
|
+
return report.ok ? 0 : 1;
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { resolveRepoRoot } from '../core/repo-root.js';
|
|
2
|
+
import { updateProdifySetup } from '../core/upgrade.js';
|
|
3
|
+
import type { CommandContext } from '../types.js';
|
|
4
|
+
|
|
5
|
+
export async function runUpdateCommand(args: string[], context: CommandContext): Promise<number> {
|
|
6
|
+
void args;
|
|
7
|
+
const repoRoot = await resolveRepoRoot({
|
|
8
|
+
cwd: context.cwd
|
|
9
|
+
});
|
|
10
|
+
const summary = await updateProdifySetup(repoRoot);
|
|
11
|
+
|
|
12
|
+
context.stdout.write('Prodify Update\n');
|
|
13
|
+
context.stdout.write(`Version/schema: ${summary.versionStatus}\n`);
|
|
14
|
+
if (summary.schemaMigrationRequired) {
|
|
15
|
+
context.stdout.write('Schema migration: applied\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
context.stdout.write(`Canonical assets: ${summary.writtenCanonicalCount} written, ${summary.preservedCanonicalCount} preserved\n`);
|
|
19
|
+
context.stdout.write(`Compiled contracts: ${summary.compiledContractCount}\n`);
|
|
20
|
+
context.stdout.write('Legacy compatibility adapters: not part of the default flow\n');
|
|
21
|
+
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Contracts Subsystem
|
|
2
|
+
|
|
3
|
+
This directory owns the contract pipeline end to end.
|
|
4
|
+
|
|
5
|
+
- `parser.ts`: parses Markdown plus YAML frontmatter into a source document.
|
|
6
|
+
- `source-schema.ts`: validates source-contract fields and normalizes them into the runtime shape.
|
|
7
|
+
- `compiled-schema.ts`: validates checked-in compiled JSON before runtime uses it.
|
|
8
|
+
- `compiler.ts`: compiles source contracts and writes runtime JSON contracts.
|
|
9
|
+
- `freshness.ts`: computes whether compiled runtime contracts are fresh relative to source contracts.
|
|
10
|
+
- `index.ts`: public entrypoint for the contract subsystem.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ProdifyError } from '../core/errors.js';
|
|
2
|
+
import { normalizeSourceContractDocument } from './source-schema.js';
|
|
3
|
+
import type { CompiledStageContract } from '../types.js';
|
|
4
|
+
|
|
5
|
+
function asString(value: unknown, fieldName: string): string {
|
|
6
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
7
|
+
throw new ProdifyError(`Compiled contract field "${fieldName}" must be a non-empty string.`, {
|
|
8
|
+
code: 'COMPILED_CONTRACT_INVALID'
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function validateCompiledContractShape(contract: unknown): CompiledStageContract {
|
|
16
|
+
if (typeof contract !== 'object' || contract === null || Array.isArray(contract)) {
|
|
17
|
+
throw new ProdifyError('Compiled contract must be a JSON object.', {
|
|
18
|
+
code: 'COMPILED_CONTRACT_INVALID'
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const record = contract as Record<string, unknown>;
|
|
23
|
+
return normalizeSourceContractDocument({
|
|
24
|
+
document: {
|
|
25
|
+
frontmatter: {
|
|
26
|
+
schema_version: record.schema_version,
|
|
27
|
+
contract_version: record.contract_version,
|
|
28
|
+
stage: record.stage,
|
|
29
|
+
task_id: record.task_id,
|
|
30
|
+
required_artifacts: record.required_artifacts,
|
|
31
|
+
allowed_write_roots: record.allowed_write_roots,
|
|
32
|
+
forbidden_writes: record.forbidden_writes,
|
|
33
|
+
policy_rules: record.policy_rules,
|
|
34
|
+
success_criteria: record.success_criteria,
|
|
35
|
+
skill_routing: record.skill_routing
|
|
36
|
+
},
|
|
37
|
+
body: 'compiled-contract'
|
|
38
|
+
},
|
|
39
|
+
sourcePath: asString(record.source_path, 'source_path'),
|
|
40
|
+
sourceHash: asString(record.source_hash, 'source_hash')
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import { listFilesRecursive, pathExists, writeFileEnsuringDir } from '../core/fs.js';
|
|
5
|
+
import { normalizeRepoRelativePath, resolveCanonicalPath } from '../core/paths.js';
|
|
6
|
+
import { ProdifyError } from '../core/errors.js';
|
|
7
|
+
import { parseContractSource } from './parser.js';
|
|
8
|
+
import { validateCompiledContractShape } from './compiled-schema.js';
|
|
9
|
+
import { CONTRACT_STAGE_NAMES, normalizeSourceContractDocument } from './source-schema.js';
|
|
10
|
+
import type { CompiledStageContract, FlowStage } from '../types.js';
|
|
11
|
+
|
|
12
|
+
function createSourceHash(source: string): string {
|
|
13
|
+
return crypto.createHash('sha256').update(source).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function serializeCompiledContract(contract: CompiledStageContract): string {
|
|
17
|
+
return `${JSON.stringify(contract, null, 2)}\n`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function compileContractSource(options: {
|
|
21
|
+
markdown: string;
|
|
22
|
+
sourcePath: string;
|
|
23
|
+
}): CompiledStageContract {
|
|
24
|
+
const { markdown, sourcePath } = options;
|
|
25
|
+
const document = parseContractSource(markdown);
|
|
26
|
+
return normalizeSourceContractDocument({
|
|
27
|
+
document,
|
|
28
|
+
sourcePath,
|
|
29
|
+
sourceHash: createSourceHash(markdown.replace(/\r\n/g, '\n'))
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function compileContractsFromSourceDir(repoRoot: string): Promise<CompiledStageContract[]> {
|
|
34
|
+
const sourceDir = resolveCanonicalPath(repoRoot, '.prodify/contracts-src');
|
|
35
|
+
if (!(await pathExists(sourceDir))) {
|
|
36
|
+
throw new ProdifyError('Contract source directory is missing: .prodify/contracts-src', {
|
|
37
|
+
code: 'CONTRACT_SOURCE_MISSING'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const files = (await listFilesRecursive(sourceDir))
|
|
42
|
+
.filter((file) => file.relativePath.endsWith('.contract.md'))
|
|
43
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
44
|
+
|
|
45
|
+
const contracts = [];
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const sourcePath = `.prodify/contracts-src/${normalizeRepoRelativePath(file.relativePath)}`;
|
|
48
|
+
contracts.push(compileContractSource({
|
|
49
|
+
markdown: await fs.readFile(file.fullPath, 'utf8'),
|
|
50
|
+
sourcePath
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const seenStages = new Set<FlowStage>();
|
|
55
|
+
for (const contract of contracts) {
|
|
56
|
+
if (seenStages.has(contract.stage)) {
|
|
57
|
+
throw new ProdifyError(`Duplicate contract stage detected: ${contract.stage}.`, {
|
|
58
|
+
code: 'CONTRACT_SCHEMA_INVALID'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
seenStages.add(contract.stage);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return contracts.sort((left, right) => left.stage.localeCompare(right.stage));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function synchronizeRuntimeContracts(repoRoot: string): Promise<CompiledStageContract[]> {
|
|
69
|
+
const contracts = await compileContractsFromSourceDir(repoRoot);
|
|
70
|
+
const compiledDir = resolveCanonicalPath(repoRoot, '.prodify/contracts');
|
|
71
|
+
|
|
72
|
+
await writeFileEnsuringDir(
|
|
73
|
+
resolveCanonicalPath(repoRoot, '.prodify/contracts/README.md'),
|
|
74
|
+
'# Compiled Contracts\n\nThis directory contains deterministic runtime-only JSON contracts generated from `.prodify/contracts-src/`.\n'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
for (const contract of contracts) {
|
|
78
|
+
await writeFileEnsuringDir(
|
|
79
|
+
resolveCanonicalPath(repoRoot, `.prodify/contracts/${contract.stage}.contract.json`),
|
|
80
|
+
serializeCompiledContract(contract)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const existingFiles = (await pathExists(compiledDir))
|
|
85
|
+
? await listFilesRecursive(compiledDir)
|
|
86
|
+
: [];
|
|
87
|
+
for (const file of existingFiles) {
|
|
88
|
+
if (!file.relativePath.endsWith('.contract.json')) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const stage = file.relativePath.replace(/\.contract\.json$/, '') as FlowStage;
|
|
93
|
+
if (!CONTRACT_STAGE_NAMES.includes(stage)) {
|
|
94
|
+
await fs.rm(file.fullPath, { force: true });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return contracts;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function loadCompiledContract(repoRoot: string, stage: FlowStage): Promise<CompiledStageContract> {
|
|
102
|
+
const contractPath = resolveCanonicalPath(repoRoot, `.prodify/contracts/${stage}.contract.json`);
|
|
103
|
+
if (!(await pathExists(contractPath))) {
|
|
104
|
+
throw new ProdifyError(`Compiled contract is missing for stage "${stage}".`, {
|
|
105
|
+
code: 'COMPILED_CONTRACT_MISSING'
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parsed = JSON.parse(await fs.readFile(contractPath, 'utf8'));
|
|
110
|
+
return validateCompiledContractShape(parsed);
|
|
111
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { compileContractsFromSourceDir, loadCompiledContract, serializeCompiledContract } from './compiler.js';
|
|
2
|
+
import { CONTRACT_STAGE_NAMES } from './source-schema.js';
|
|
3
|
+
import type { CompiledContractInventory, CompiledStageContract, FlowStage } from '../types.js';
|
|
4
|
+
|
|
5
|
+
export async function inspectCompiledContracts(repoRoot: string): Promise<CompiledContractInventory> {
|
|
6
|
+
const inventory: CompiledContractInventory = {
|
|
7
|
+
ok: true,
|
|
8
|
+
sourceCount: 0,
|
|
9
|
+
compiledCount: 0,
|
|
10
|
+
staleStages: [],
|
|
11
|
+
missingCompiledStages: [],
|
|
12
|
+
missingSourceStages: [],
|
|
13
|
+
invalidStages: []
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let expectedContracts: CompiledStageContract[] = [];
|
|
17
|
+
try {
|
|
18
|
+
expectedContracts = await compileContractsFromSourceDir(repoRoot);
|
|
19
|
+
inventory.sourceCount = expectedContracts.length;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
inventory.ok = false;
|
|
22
|
+
inventory.invalidStages.push(error instanceof Error ? error.message : String(error));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const expectedByStage = new Map<FlowStage, CompiledStageContract>();
|
|
26
|
+
for (const contract of expectedContracts) {
|
|
27
|
+
expectedByStage.set(contract.stage, contract);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const stage of CONTRACT_STAGE_NAMES) {
|
|
31
|
+
const expected = expectedByStage.get(stage);
|
|
32
|
+
if (!expected) {
|
|
33
|
+
inventory.ok = false;
|
|
34
|
+
inventory.missingSourceStages.push(stage);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const compiled = await loadCompiledContract(repoRoot, stage);
|
|
40
|
+
inventory.compiledCount += 1;
|
|
41
|
+
if (serializeCompiledContract(compiled) !== serializeCompiledContract(expected)) {
|
|
42
|
+
inventory.ok = false;
|
|
43
|
+
inventory.staleStages.push(stage);
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
inventory.ok = false;
|
|
47
|
+
inventory.missingCompiledStages.push(stage);
|
|
48
|
+
inventory.invalidStages.push(error instanceof Error ? error.message : String(error));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
inventory.staleStages.sort((left, right) => left.localeCompare(right));
|
|
53
|
+
inventory.missingCompiledStages.sort((left, right) => left.localeCompare(right));
|
|
54
|
+
inventory.missingSourceStages.sort((left, right) => left.localeCompare(right));
|
|
55
|
+
inventory.invalidStages.sort((left, right) => left.localeCompare(right));
|
|
56
|
+
|
|
57
|
+
return inventory;
|
|
58
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { parseContractSource } from './parser.js';
|
|
2
|
+
export { normalizeSourceContractDocument, CONTRACT_STAGE_NAMES } from './source-schema.js';
|
|
3
|
+
export { validateCompiledContractShape } from './compiled-schema.js';
|
|
4
|
+
export {
|
|
5
|
+
compileContractSource,
|
|
6
|
+
compileContractsFromSourceDir,
|
|
7
|
+
synchronizeRuntimeContracts,
|
|
8
|
+
loadCompiledContract,
|
|
9
|
+
serializeCompiledContract
|
|
10
|
+
} from './compiler.js';
|
|
11
|
+
export { inspectCompiledContracts } from './freshness.js';
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { ProdifyError } from '../core/errors.js';
|
|
2
|
+
import type { ContractSourceDocument } from '../types.js';
|
|
3
|
+
|
|
4
|
+
interface ParsedLine {
|
|
5
|
+
indent: number;
|
|
6
|
+
content: string;
|
|
7
|
+
lineNumber: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseScalar(rawValue: string): unknown {
|
|
11
|
+
const value = rawValue.trim();
|
|
12
|
+
|
|
13
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) {
|
|
14
|
+
return value.slice(1, -1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (value === 'true') {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (value === 'false') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (value === 'null') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
30
|
+
return Number(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function splitKeyValue(content: string, lineNumber: number): { key: string; value: string } {
|
|
37
|
+
const separatorIndex = content.indexOf(':');
|
|
38
|
+
if (separatorIndex === -1) {
|
|
39
|
+
throw new ProdifyError(`Invalid frontmatter at line ${lineNumber}: expected "key: value".`, {
|
|
40
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const key = content.slice(0, separatorIndex).trim();
|
|
45
|
+
if (!key) {
|
|
46
|
+
throw new ProdifyError(`Invalid frontmatter at line ${lineNumber}: missing key before ":".`, {
|
|
47
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
key,
|
|
53
|
+
value: content.slice(separatorIndex + 1).trim()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function preprocessFrontmatter(frontmatter: string): ParsedLine[] {
|
|
58
|
+
return frontmatter
|
|
59
|
+
.split('\n')
|
|
60
|
+
.map((line, index) => ({ raw: line.replace(/\r$/, ''), lineNumber: index + 1 }))
|
|
61
|
+
.filter(({ raw }) => raw.trim() !== '' && !raw.trimStart().startsWith('#'))
|
|
62
|
+
.map(({ raw, lineNumber }) => {
|
|
63
|
+
const indent = raw.match(/^ */)?.[0].length ?? 0;
|
|
64
|
+
if (indent % 2 !== 0) {
|
|
65
|
+
throw new ProdifyError(`Invalid frontmatter indentation at line ${lineNumber}: use multiples of two spaces.`, {
|
|
66
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
indent,
|
|
72
|
+
content: raw.trim(),
|
|
73
|
+
lineNumber
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseBlock(lines: ParsedLine[], state: { index: number }, indent: number): unknown {
|
|
79
|
+
const line = lines[state.index];
|
|
80
|
+
if (!line || line.indent !== indent) {
|
|
81
|
+
throw new ProdifyError(`Invalid frontmatter near line ${line?.lineNumber ?? 'EOF'}: unexpected indentation.`, {
|
|
82
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (line.content.startsWith('- ')) {
|
|
87
|
+
return parseSequence(lines, state, indent);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return parseMapping(lines, state, indent);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseMapping(lines: ParsedLine[], state: { index: number }, indent: number): Record<string, unknown> {
|
|
94
|
+
const value: Record<string, unknown> = {};
|
|
95
|
+
|
|
96
|
+
while (state.index < lines.length) {
|
|
97
|
+
const line = lines[state.index];
|
|
98
|
+
if (line.indent < indent) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (line.indent > indent) {
|
|
103
|
+
throw new ProdifyError(`Invalid frontmatter at line ${line.lineNumber}: unexpected indentation.`, {
|
|
104
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (line.content.startsWith('- ')) {
|
|
109
|
+
throw new ProdifyError(`Invalid frontmatter at line ${line.lineNumber}: sequence item is not valid here.`, {
|
|
110
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { key, value: rawValue } = splitKeyValue(line.content, line.lineNumber);
|
|
115
|
+
state.index += 1;
|
|
116
|
+
|
|
117
|
+
if (rawValue !== '') {
|
|
118
|
+
value[key] = parseScalar(rawValue);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const nextLine = lines[state.index];
|
|
123
|
+
if (!nextLine || nextLine.indent <= indent) {
|
|
124
|
+
throw new ProdifyError(`Invalid frontmatter at line ${line.lineNumber}: expected an indented block for "${key}".`, {
|
|
125
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
value[key] = parseBlock(lines, state, indent + 2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseSequence(lines: ParsedLine[], state: { index: number }, indent: number): unknown[] {
|
|
136
|
+
const value: unknown[] = [];
|
|
137
|
+
|
|
138
|
+
while (state.index < lines.length) {
|
|
139
|
+
const line = lines[state.index];
|
|
140
|
+
if (line.indent < indent) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (line.indent > indent) {
|
|
145
|
+
throw new ProdifyError(`Invalid frontmatter at line ${line.lineNumber}: unexpected indentation.`, {
|
|
146
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!line.content.startsWith('- ')) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const inlineValue = line.content.slice(2).trim();
|
|
155
|
+
state.index += 1;
|
|
156
|
+
|
|
157
|
+
if (inlineValue === '') {
|
|
158
|
+
const nextLine = lines[state.index];
|
|
159
|
+
if (!nextLine || nextLine.indent <= indent) {
|
|
160
|
+
throw new ProdifyError(`Invalid frontmatter at line ${line.lineNumber}: expected an indented sequence value.`, {
|
|
161
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
value.push(parseBlock(lines, state, indent + 2));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (inlineValue.includes(':')) {
|
|
170
|
+
const { key, value: rawValue } = splitKeyValue(inlineValue, line.lineNumber);
|
|
171
|
+
const item: Record<string, unknown> = {};
|
|
172
|
+
|
|
173
|
+
if (rawValue !== '') {
|
|
174
|
+
item[key] = parseScalar(rawValue);
|
|
175
|
+
} else {
|
|
176
|
+
const nextLine = lines[state.index];
|
|
177
|
+
if (!nextLine || nextLine.indent <= indent) {
|
|
178
|
+
throw new ProdifyError(`Invalid frontmatter at line ${line.lineNumber}: expected an indented block for "${key}".`, {
|
|
179
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
item[key] = parseBlock(lines, state, indent + 2);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (state.index < lines.length && lines[state.index].indent > indent) {
|
|
187
|
+
Object.assign(item, parseMapping(lines, state, indent + 2));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
value.push(item);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
value.push(parseScalar(inlineValue));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseFrontmatter(frontmatter: string): Record<string, unknown> {
|
|
201
|
+
const lines = preprocessFrontmatter(frontmatter);
|
|
202
|
+
if (lines.length === 0) {
|
|
203
|
+
throw new ProdifyError('Contract source frontmatter is empty.', {
|
|
204
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const state = { index: 0 };
|
|
209
|
+
const parsed = parseBlock(lines, state, 0);
|
|
210
|
+
if (state.index !== lines.length) {
|
|
211
|
+
const nextLine = lines[state.index];
|
|
212
|
+
throw new ProdifyError(`Invalid frontmatter at line ${nextLine.lineNumber}: trailing content could not be parsed.`, {
|
|
213
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (Array.isArray(parsed)) {
|
|
218
|
+
throw new ProdifyError('Contract source frontmatter must be a mapping.', {
|
|
219
|
+
code: 'CONTRACT_FRONTMATTER_INVALID'
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return parsed as Record<string, unknown>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function parseContractSource(markdown: string): ContractSourceDocument {
|
|
227
|
+
const normalized = markdown.replace(/\r\n/g, '\n');
|
|
228
|
+
if (!normalized.startsWith('---\n')) {
|
|
229
|
+
throw new ProdifyError('Contract source must start with YAML frontmatter delimited by "---".', {
|
|
230
|
+
code: 'CONTRACT_FRONTMATTER_MISSING'
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const closingIndex = normalized.indexOf('\n---\n', 4);
|
|
235
|
+
if (closingIndex === -1) {
|
|
236
|
+
throw new ProdifyError('Contract source frontmatter is not closed with "---".', {
|
|
237
|
+
code: 'CONTRACT_FRONTMATTER_MISSING'
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const rawFrontmatter = normalized.slice(4, closingIndex);
|
|
242
|
+
const body = normalized.slice(closingIndex + 5).trim();
|
|
243
|
+
if (!body) {
|
|
244
|
+
throw new ProdifyError('Contract source body is empty.', {
|
|
245
|
+
code: 'CONTRACT_BODY_EMPTY'
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
frontmatter: parseFrontmatter(rawFrontmatter),
|
|
251
|
+
body
|
|
252
|
+
};
|
|
253
|
+
}
|