@hongmaple0820/scale-engine 0.17.0 → 0.19.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/README.en.md +296 -237
- package/README.md +157 -63
- package/dist/api/cli.js +661 -33
- package/dist/api/cli.js.map +1 -1
- package/dist/api/doctor.d.ts +5 -1
- package/dist/api/doctor.js +130 -1
- package/dist/api/doctor.js.map +1 -1
- package/dist/api/quickstart.d.ts +3 -0
- package/dist/api/quickstart.js +12 -4
- package/dist/api/quickstart.js.map +1 -1
- package/dist/cli/phaseCommands.js +7 -0
- package/dist/cli/phaseCommands.js.map +1 -1
- package/dist/core/logger.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/memory/MemoryFabric.d.ts +118 -0
- package/dist/memory/MemoryFabric.js +281 -0
- package/dist/memory/MemoryFabric.js.map +1 -0
- package/dist/memory/MemoryLearning.d.ts +61 -0
- package/dist/memory/MemoryLearning.js +203 -0
- package/dist/memory/MemoryLearning.js.map +1 -0
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +3 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/output/HTMLArtifactLayer.d.ts +97 -0
- package/dist/output/HTMLArtifactLayer.js +576 -0
- package/dist/output/HTMLArtifactLayer.js.map +1 -0
- package/dist/output/index.d.ts +2 -0
- package/dist/output/index.js +1 -0
- package/dist/output/index.js.map +1 -1
- package/dist/prompts/VibeTemplateGallery.js +121 -121
- package/dist/runtime/FinalReportGuard.d.ts +16 -0
- package/dist/runtime/FinalReportGuard.js +14 -0
- package/dist/runtime/FinalReportGuard.js.map +1 -0
- package/dist/runtime/RuntimeDoctor.d.ts +23 -0
- package/dist/runtime/RuntimeDoctor.js +151 -0
- package/dist/runtime/RuntimeDoctor.js.map +1 -0
- package/dist/runtime/RuntimeEvidenceLedger.d.ts +50 -0
- package/dist/runtime/RuntimeEvidenceLedger.js +89 -0
- package/dist/runtime/RuntimeEvidenceLedger.js.map +1 -0
- package/dist/runtime/SessionLedger.d.ts +53 -0
- package/dist/runtime/SessionLedger.js +104 -0
- package/dist/runtime/SessionLedger.js.map +1 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/skills/routing/SkillGate.js +26 -2
- package/dist/skills/routing/SkillGate.js.map +1 -1
- package/dist/skills/routing/SkillPolicy.js +2 -2
- package/dist/skills/routing/SkillPolicy.js.map +1 -1
- package/dist/tools/ToolCapabilityRegistry.d.ts +1 -1
- package/dist/tools/ToolCapabilityRegistry.js +4 -4
- package/dist/tools/ToolCapabilityRegistry.js.map +1 -1
- package/dist/tools/ToolOrchestrator.js +5 -1
- package/dist/tools/ToolOrchestrator.js.map +1 -1
- package/dist/workflow/EngineeringStandards.js +69 -66
- package/dist/workflow/EngineeringStandards.js.map +1 -1
- package/dist/workflow/GovernanceTemplatePacks.d.ts +1 -1
- package/dist/workflow/GovernanceTemplatePacks.js +137 -79
- package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
- package/dist/workflow/GovernanceTemplates.d.ts +1 -1
- package/dist/workflow/GovernanceTemplates.js +494 -199
- package/dist/workflow/GovernanceTemplates.js.map +1 -1
- package/dist/workflow/ResourceGovernance.js +29 -19
- package/dist/workflow/ResourceGovernance.js.map +1 -1
- package/dist/workflow/VerificationCommands.d.ts +11 -0
- package/dist/workflow/VerificationCommands.js +2 -0
- package/dist/workflow/VerificationCommands.js.map +1 -1
- package/dist/workflow/VerificationProfile.d.ts +2 -1
- package/dist/workflow/VerificationProfile.js +3 -0
- package/dist/workflow/VerificationProfile.js.map +1 -1
- package/dist/workflow/WorkflowArtifactWriter.js +2 -1
- package/dist/workflow/WorkflowArtifactWriter.js.map +1 -1
- package/dist/workflow/WorkflowEngine.js +4 -1
- package/dist/workflow/WorkflowEngine.js.map +1 -1
- package/dist/workflow/WorkspaceSafety.d.ts +9 -0
- package/dist/workflow/WorkspaceSafety.js +49 -0
- package/dist/workflow/WorkspaceSafety.js.map +1 -0
- package/dist/workflow/gates/GateSystem.d.ts +12 -1
- package/dist/workflow/gates/GateSystem.js +106 -0
- package/dist/workflow/gates/GateSystem.js.map +1 -1
- package/dist/workflow/types.d.ts +1 -1
- package/docs/MEMORY_FABRIC.md +107 -0
- package/docs/README.md +68 -0
- package/docs/RUNTIME_EVIDENCE.md +101 -0
- package/docs/start/README.md +42 -0
- package/docs/start/agent-governance-demo.md +107 -0
- package/docs/start/quickstart.md +127 -0
- package/examples/demo-projects/agent-governance-demo/README.md +37 -0
- package/examples/demo-projects/agent-governance-demo/package.json +16 -0
- package/examples/demo-projects/agent-governance-demo/src/oauth-state.ts +39 -0
- package/examples/demo-projects/agent-governance-demo/tests/oauth-state.test.ts +52 -0
- package/package.json +8 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type DocLang, type ThemeMode } from './HTMLDocumentRenderer.js';
|
|
2
|
+
export declare const HTML_ARTIFACT_TYPES: readonly ["plan-comparison", "implementation-plan", "code-review", "status-report", "incident-report", "release-report"];
|
|
3
|
+
export type HtmlArtifactType = typeof HTML_ARTIFACT_TYPES[number];
|
|
4
|
+
export interface HtmlArtifactPolicyTemplate {
|
|
5
|
+
label: string;
|
|
6
|
+
sources: string[];
|
|
7
|
+
description: string;
|
|
8
|
+
}
|
|
9
|
+
export interface HtmlArtifactPolicy {
|
|
10
|
+
version: number;
|
|
11
|
+
sourceFormat: 'markdown';
|
|
12
|
+
artifactDirectory: string;
|
|
13
|
+
manifestFile: string;
|
|
14
|
+
defaultTheme: ThemeMode;
|
|
15
|
+
defaultGitPolicy: 'review' | 'ignore' | 'commit' | 'external';
|
|
16
|
+
safety: {
|
|
17
|
+
allowRemoteScripts: boolean;
|
|
18
|
+
allowRemoteStyles: boolean;
|
|
19
|
+
detectSecrets: boolean;
|
|
20
|
+
};
|
|
21
|
+
templates: Record<HtmlArtifactType, HtmlArtifactPolicyTemplate>;
|
|
22
|
+
}
|
|
23
|
+
export interface HtmlArtifactManifestEntry {
|
|
24
|
+
type: HtmlArtifactType;
|
|
25
|
+
title: string;
|
|
26
|
+
path: string;
|
|
27
|
+
sourcePaths: string[];
|
|
28
|
+
missingSources: string[];
|
|
29
|
+
gitPolicy: HtmlArtifactPolicy['defaultGitPolicy'];
|
|
30
|
+
generatedAt: string;
|
|
31
|
+
renderer: 'scale-engine';
|
|
32
|
+
}
|
|
33
|
+
export interface HtmlArtifactManifest {
|
|
34
|
+
version: number;
|
|
35
|
+
taskId?: string;
|
|
36
|
+
generatedAt: string;
|
|
37
|
+
artifactDirectory: string;
|
|
38
|
+
artifacts: HtmlArtifactManifestEntry[];
|
|
39
|
+
}
|
|
40
|
+
export interface RenderHtmlArtifactOptions {
|
|
41
|
+
projectDir?: string;
|
|
42
|
+
scaleDir?: string;
|
|
43
|
+
taskId?: string;
|
|
44
|
+
artifactDir?: string;
|
|
45
|
+
type?: HtmlArtifactType | string;
|
|
46
|
+
sourcePaths?: string[];
|
|
47
|
+
theme?: ThemeMode;
|
|
48
|
+
lang?: DocLang;
|
|
49
|
+
title?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface RenderHtmlArtifactResult {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
type: HtmlArtifactType;
|
|
54
|
+
taskDir: string;
|
|
55
|
+
outputPath: string;
|
|
56
|
+
indexPath: string;
|
|
57
|
+
manifestPath: string;
|
|
58
|
+
sourcePaths: string[];
|
|
59
|
+
missingSources: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface HtmlArtifactFinding {
|
|
62
|
+
severity: 'warn' | 'fail';
|
|
63
|
+
code: string;
|
|
64
|
+
path?: string;
|
|
65
|
+
message: string;
|
|
66
|
+
fix?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface HtmlArtifactDoctorReport {
|
|
69
|
+
ok: boolean;
|
|
70
|
+
projectDir: string;
|
|
71
|
+
taskDir: string;
|
|
72
|
+
manifestPath: string;
|
|
73
|
+
findings: HtmlArtifactFinding[];
|
|
74
|
+
artifacts: HtmlArtifactManifestEntry[];
|
|
75
|
+
}
|
|
76
|
+
export interface SettleHtmlArtifactsOptions {
|
|
77
|
+
projectDir?: string;
|
|
78
|
+
scaleDir?: string;
|
|
79
|
+
taskId?: string;
|
|
80
|
+
artifactDir?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface SettleHtmlArtifactsReport {
|
|
83
|
+
ok: boolean;
|
|
84
|
+
taskId?: string;
|
|
85
|
+
htmlImpactPath: string;
|
|
86
|
+
doctor: HtmlArtifactDoctorReport;
|
|
87
|
+
}
|
|
88
|
+
export declare function outputPolicyPath(projectDir?: string, scaleDir?: string): string;
|
|
89
|
+
export declare function outputPolicyTemplate(): string;
|
|
90
|
+
export declare function defaultHtmlArtifactPolicy(): HtmlArtifactPolicy;
|
|
91
|
+
export declare function loadHtmlArtifactPolicy(projectDir?: string, scaleDir?: string): HtmlArtifactPolicy;
|
|
92
|
+
export declare function renderHtmlArtifact(options?: RenderHtmlArtifactOptions): RenderHtmlArtifactResult;
|
|
93
|
+
export declare function doctorHtmlArtifacts(options?: RenderHtmlArtifactOptions): HtmlArtifactDoctorReport;
|
|
94
|
+
export declare function settleHtmlArtifacts(options?: SettleHtmlArtifactsOptions): SettleHtmlArtifactsReport;
|
|
95
|
+
export declare function resolveHtmlArtifactForOpen(options?: RenderHtmlArtifactOptions): string;
|
|
96
|
+
export declare function normalizeHtmlArtifactType(value: string): HtmlArtifactType;
|
|
97
|
+
export declare function listExistingHtmlArtifacts(options?: RenderHtmlArtifactOptions): string[];
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { HTMLDocumentRenderer } from './HTMLDocumentRenderer.js';
|
|
4
|
+
export const HTML_ARTIFACT_TYPES = [
|
|
5
|
+
'plan-comparison',
|
|
6
|
+
'implementation-plan',
|
|
7
|
+
'code-review',
|
|
8
|
+
'status-report',
|
|
9
|
+
'incident-report',
|
|
10
|
+
'release-report',
|
|
11
|
+
];
|
|
12
|
+
const DEFAULT_TEMPLATE_SOURCES = {
|
|
13
|
+
'plan-comparison': {
|
|
14
|
+
label: 'Plan Comparison',
|
|
15
|
+
sources: ['mini-prd.md', 'explore.md', 'plan.md'],
|
|
16
|
+
description: 'Compare candidate approaches, tradeoffs, open questions, and decision criteria.',
|
|
17
|
+
},
|
|
18
|
+
'implementation-plan': {
|
|
19
|
+
label: 'Implementation Plan',
|
|
20
|
+
sources: ['plan.md', 'verification.md'],
|
|
21
|
+
description: 'Convert the implementation plan and verification strategy into a scannable delivery surface.',
|
|
22
|
+
},
|
|
23
|
+
'code-review': {
|
|
24
|
+
label: 'Code Review',
|
|
25
|
+
sources: ['review.md', 'security-review.md', 'standards-impact.md'],
|
|
26
|
+
description: 'Summarize review findings, severity, evidence, and residual risks.',
|
|
27
|
+
},
|
|
28
|
+
'status-report': {
|
|
29
|
+
label: 'Status Report',
|
|
30
|
+
sources: ['summary.md', 'verification.md', 'resource-impact.md', 'standards-impact.md'],
|
|
31
|
+
description: 'Show current task status, proof, blockers, resource state, and follow-ups.',
|
|
32
|
+
},
|
|
33
|
+
'incident-report': {
|
|
34
|
+
label: 'Incident Report',
|
|
35
|
+
sources: ['explore.md', 'plan.md', 'verification.md', 'review.md'],
|
|
36
|
+
description: 'Explain incident context, diagnosis, fix, validation, and prevention work.',
|
|
37
|
+
},
|
|
38
|
+
'release-report': {
|
|
39
|
+
label: 'Release Report',
|
|
40
|
+
sources: ['summary.md', 'verification.md', 'review.md', 'resource-impact.md', 'standards-impact.md'],
|
|
41
|
+
description: 'Package final release evidence, risk state, unverified items, and sign-off readiness.',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
export function outputPolicyPath(projectDir = process.cwd(), scaleDir = '.scale') {
|
|
45
|
+
return join(projectDir, scaleDir, 'output-policy.json');
|
|
46
|
+
}
|
|
47
|
+
export function outputPolicyTemplate() {
|
|
48
|
+
return JSON.stringify(defaultHtmlArtifactPolicy(), null, 2) + '\n';
|
|
49
|
+
}
|
|
50
|
+
export function defaultHtmlArtifactPolicy() {
|
|
51
|
+
return {
|
|
52
|
+
version: 1,
|
|
53
|
+
sourceFormat: 'markdown',
|
|
54
|
+
artifactDirectory: 'artifacts',
|
|
55
|
+
manifestFile: 'artifact-manifest.json',
|
|
56
|
+
defaultTheme: 'auto',
|
|
57
|
+
defaultGitPolicy: 'review',
|
|
58
|
+
safety: {
|
|
59
|
+
allowRemoteScripts: false,
|
|
60
|
+
allowRemoteStyles: false,
|
|
61
|
+
detectSecrets: true,
|
|
62
|
+
},
|
|
63
|
+
templates: DEFAULT_TEMPLATE_SOURCES,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function loadHtmlArtifactPolicy(projectDir = process.cwd(), scaleDir = '.scale') {
|
|
67
|
+
const defaults = defaultHtmlArtifactPolicy();
|
|
68
|
+
const path = outputPolicyPath(projectDir, scaleDir);
|
|
69
|
+
if (!existsSync(path))
|
|
70
|
+
return defaults;
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
73
|
+
return {
|
|
74
|
+
...defaults,
|
|
75
|
+
...parsed,
|
|
76
|
+
safety: {
|
|
77
|
+
...defaults.safety,
|
|
78
|
+
...(parsed.safety ?? {}),
|
|
79
|
+
},
|
|
80
|
+
templates: {
|
|
81
|
+
...defaults.templates,
|
|
82
|
+
...(parsed.templates ?? {}),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return defaults;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function renderHtmlArtifact(options = {}) {
|
|
91
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
92
|
+
const scaleDir = options.scaleDir ?? '.scale';
|
|
93
|
+
const policy = loadHtmlArtifactPolicy(projectDir, scaleDir);
|
|
94
|
+
const type = normalizeHtmlArtifactType(options.type ?? 'release-report');
|
|
95
|
+
const taskDir = resolveTaskDir(projectDir, options.taskId, options.artifactDir);
|
|
96
|
+
const template = policy.templates[type] ?? DEFAULT_TEMPLATE_SOURCES[type];
|
|
97
|
+
const sourcePaths = normalizeSourcePaths(options.sourcePaths?.length ? options.sourcePaths : template.sources);
|
|
98
|
+
const sourceSections = readSourceSections(taskDir, sourcePaths);
|
|
99
|
+
const title = options.title ?? `${template.label} - ${options.taskId ?? basename(taskDir)}`;
|
|
100
|
+
const outputDir = join(taskDir, policy.artifactDirectory);
|
|
101
|
+
if (!existsSync(outputDir))
|
|
102
|
+
mkdirSync(outputDir, { recursive: true });
|
|
103
|
+
const outputPath = join(outputDir, `${type}.html`);
|
|
104
|
+
const renderer = new HTMLDocumentRenderer({
|
|
105
|
+
title,
|
|
106
|
+
theme: options.theme ?? policy.defaultTheme,
|
|
107
|
+
lang: options.lang ?? 'zh',
|
|
108
|
+
interactive: true,
|
|
109
|
+
printFriendly: true,
|
|
110
|
+
});
|
|
111
|
+
const sections = [
|
|
112
|
+
{
|
|
113
|
+
heading: 'Purpose',
|
|
114
|
+
content: renderPurposeSection(template, sourceSections),
|
|
115
|
+
},
|
|
116
|
+
...sourceSections.present.map(section => ({
|
|
117
|
+
heading: section.heading,
|
|
118
|
+
content: markdownToHtml(section.content),
|
|
119
|
+
})),
|
|
120
|
+
];
|
|
121
|
+
const html = renderer.renderReport({
|
|
122
|
+
type,
|
|
123
|
+
title,
|
|
124
|
+
timestamp: new Date().toISOString(),
|
|
125
|
+
metrics: {
|
|
126
|
+
sources: sourceSections.present.length,
|
|
127
|
+
missing: sourceSections.missing.length,
|
|
128
|
+
policy: policy.defaultGitPolicy,
|
|
129
|
+
},
|
|
130
|
+
sections,
|
|
131
|
+
});
|
|
132
|
+
writeFileSync(outputPath, html, 'utf-8');
|
|
133
|
+
const manifestPath = join(taskDir, policy.manifestFile);
|
|
134
|
+
const manifest = upsertManifestEntry({
|
|
135
|
+
manifestPath,
|
|
136
|
+
taskId: options.taskId,
|
|
137
|
+
artifactDirectory: policy.artifactDirectory,
|
|
138
|
+
entry: {
|
|
139
|
+
type,
|
|
140
|
+
title,
|
|
141
|
+
path: normalizePath(relative(projectDir, outputPath)),
|
|
142
|
+
sourcePaths: sourceSections.present.map(section => section.relativePath),
|
|
143
|
+
missingSources: sourceSections.missing,
|
|
144
|
+
gitPolicy: policy.defaultGitPolicy,
|
|
145
|
+
generatedAt: new Date().toISOString(),
|
|
146
|
+
renderer: 'scale-engine',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const indexPath = writeArtifactIndex(projectDir, taskDir, policy, manifest);
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
type,
|
|
153
|
+
taskDir,
|
|
154
|
+
outputPath,
|
|
155
|
+
indexPath,
|
|
156
|
+
manifestPath,
|
|
157
|
+
sourcePaths: sourceSections.present.map(section => section.relativePath),
|
|
158
|
+
missingSources: sourceSections.missing,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export function doctorHtmlArtifacts(options = {}) {
|
|
162
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
163
|
+
const policy = loadHtmlArtifactPolicy(projectDir, options.scaleDir ?? '.scale');
|
|
164
|
+
const taskDir = resolveTaskDir(projectDir, options.taskId, options.artifactDir);
|
|
165
|
+
const manifestPath = join(taskDir, policy.manifestFile);
|
|
166
|
+
const findings = [];
|
|
167
|
+
const manifest = readManifest(manifestPath);
|
|
168
|
+
if (!manifest) {
|
|
169
|
+
findings.push({
|
|
170
|
+
severity: 'fail',
|
|
171
|
+
code: 'missing-manifest',
|
|
172
|
+
path: normalizePath(relative(projectDir, manifestPath)),
|
|
173
|
+
message: 'HTML artifact manifest is missing.',
|
|
174
|
+
fix: 'Run scale artifact render --task-id <task> --type release-report.',
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
projectDir,
|
|
179
|
+
taskDir,
|
|
180
|
+
manifestPath,
|
|
181
|
+
findings,
|
|
182
|
+
artifacts: [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const selectedType = options.type ? normalizeHtmlArtifactType(options.type) : undefined;
|
|
186
|
+
const artifacts = selectedType
|
|
187
|
+
? manifest.artifacts.filter(artifact => artifact.type === selectedType)
|
|
188
|
+
: manifest.artifacts;
|
|
189
|
+
if (artifacts.length === 0) {
|
|
190
|
+
findings.push({
|
|
191
|
+
severity: 'fail',
|
|
192
|
+
code: 'missing-artifact-entry',
|
|
193
|
+
message: selectedType
|
|
194
|
+
? `Manifest has no entry for ${selectedType}.`
|
|
195
|
+
: 'Manifest has no artifact entries.',
|
|
196
|
+
fix: 'Render the required HTML artifact before review or release.',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
for (const artifact of artifacts) {
|
|
200
|
+
const absoluteArtifactPath = resolve(projectDir, artifact.path);
|
|
201
|
+
if (!existsSync(absoluteArtifactPath)) {
|
|
202
|
+
findings.push({
|
|
203
|
+
severity: 'fail',
|
|
204
|
+
code: 'missing-html-artifact',
|
|
205
|
+
path: artifact.path,
|
|
206
|
+
message: 'Manifest points to an HTML artifact that does not exist.',
|
|
207
|
+
fix: `Re-render ${artifact.type} or remove the stale manifest entry.`,
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const html = readFileSync(absoluteArtifactPath, 'utf-8');
|
|
212
|
+
findings.push(...checkHtmlSafety(html, artifact.path, policy));
|
|
213
|
+
findings.push(...checkSourceFreshness(projectDir, absoluteArtifactPath, artifact));
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
ok: !findings.some(finding => finding.severity === 'fail'),
|
|
217
|
+
projectDir,
|
|
218
|
+
taskDir,
|
|
219
|
+
manifestPath,
|
|
220
|
+
findings,
|
|
221
|
+
artifacts,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
export function settleHtmlArtifacts(options = {}) {
|
|
225
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
226
|
+
const doctor = doctorHtmlArtifacts(options);
|
|
227
|
+
const taskDir = resolveTaskDir(projectDir, options.taskId, options.artifactDir);
|
|
228
|
+
if (!existsSync(taskDir))
|
|
229
|
+
mkdirSync(taskDir, { recursive: true });
|
|
230
|
+
const htmlImpactPath = join(taskDir, 'html-artifacts.md');
|
|
231
|
+
writeFileSync(htmlImpactPath, htmlArtifactSettlementMarkdown(options.taskId, doctor), 'utf-8');
|
|
232
|
+
return {
|
|
233
|
+
ok: doctor.ok,
|
|
234
|
+
taskId: options.taskId,
|
|
235
|
+
htmlImpactPath,
|
|
236
|
+
doctor,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
export function resolveHtmlArtifactForOpen(options = {}) {
|
|
240
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
241
|
+
const policy = loadHtmlArtifactPolicy(projectDir, options.scaleDir ?? '.scale');
|
|
242
|
+
const taskDir = resolveTaskDir(projectDir, options.taskId, options.artifactDir);
|
|
243
|
+
const manifest = readManifest(join(taskDir, policy.manifestFile));
|
|
244
|
+
const selectedType = options.type ? normalizeHtmlArtifactType(options.type) : undefined;
|
|
245
|
+
const artifact = selectedType
|
|
246
|
+
? manifest?.artifacts.find(item => item.type === selectedType)
|
|
247
|
+
: manifest?.artifacts[manifest.artifacts.length - 1];
|
|
248
|
+
if (artifact)
|
|
249
|
+
return resolve(projectDir, artifact.path);
|
|
250
|
+
const fallbackType = selectedType ?? 'release-report';
|
|
251
|
+
return join(taskDir, policy.artifactDirectory, `${fallbackType}.html`);
|
|
252
|
+
}
|
|
253
|
+
export function normalizeHtmlArtifactType(value) {
|
|
254
|
+
const normalized = value.trim().toLowerCase();
|
|
255
|
+
if (HTML_ARTIFACT_TYPES.includes(normalized))
|
|
256
|
+
return normalized;
|
|
257
|
+
if (normalized === 'plan')
|
|
258
|
+
return 'implementation-plan';
|
|
259
|
+
if (normalized === 'review')
|
|
260
|
+
return 'code-review';
|
|
261
|
+
if (normalized === 'status')
|
|
262
|
+
return 'status-report';
|
|
263
|
+
if (normalized === 'incident')
|
|
264
|
+
return 'incident-report';
|
|
265
|
+
if (normalized === 'release')
|
|
266
|
+
return 'release-report';
|
|
267
|
+
throw new Error(`Unknown HTML artifact type "${value}". Supported types: ${HTML_ARTIFACT_TYPES.join(', ')}`);
|
|
268
|
+
}
|
|
269
|
+
function resolveTaskDir(projectDir, taskId, artifactDir) {
|
|
270
|
+
if (artifactDir?.trim()) {
|
|
271
|
+
return isAbsolute(artifactDir)
|
|
272
|
+
? artifactDir
|
|
273
|
+
: resolve(projectDir, artifactDir);
|
|
274
|
+
}
|
|
275
|
+
if (taskId?.trim()) {
|
|
276
|
+
return join(projectDir, 'docs', 'worklog', 'tasks', taskId.trim());
|
|
277
|
+
}
|
|
278
|
+
return projectDir;
|
|
279
|
+
}
|
|
280
|
+
function normalizeSourcePaths(sourcePaths) {
|
|
281
|
+
return sourcePaths
|
|
282
|
+
.map(item => normalizePath(item.trim()))
|
|
283
|
+
.filter(Boolean);
|
|
284
|
+
}
|
|
285
|
+
function readSourceSections(taskDir, sourcePaths) {
|
|
286
|
+
const present = [];
|
|
287
|
+
const missing = [];
|
|
288
|
+
for (const sourcePath of sourcePaths) {
|
|
289
|
+
const absolutePath = resolve(taskDir, sourcePath);
|
|
290
|
+
if (!absolutePath.startsWith(resolve(taskDir))) {
|
|
291
|
+
missing.push(sourcePath);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (!existsSync(absolutePath)) {
|
|
295
|
+
missing.push(sourcePath);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
present.push({
|
|
299
|
+
heading: sourceHeading(sourcePath),
|
|
300
|
+
relativePath: sourcePath,
|
|
301
|
+
content: readFileSync(absolutePath, 'utf-8'),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return { present, missing };
|
|
305
|
+
}
|
|
306
|
+
function renderPurposeSection(template, sections) {
|
|
307
|
+
const present = sections.present.length
|
|
308
|
+
? `<ul>${sections.present.map(section => `<li><code>${escapeHtml(section.relativePath)}</code></li>`).join('')}</ul>`
|
|
309
|
+
: '<p>No source artifacts were found.</p>';
|
|
310
|
+
const missing = sections.missing.length
|
|
311
|
+
? `<p class="doc-warning">Missing source artifacts: ${sections.missing.map(item => `<code>${escapeHtml(item)}</code>`).join(', ')}</p>`
|
|
312
|
+
: '<p>All configured source artifacts were found.</p>';
|
|
313
|
+
return `
|
|
314
|
+
<p>${escapeHtml(template.description)}</p>
|
|
315
|
+
<h3>Source Artifacts</h3>
|
|
316
|
+
${present}
|
|
317
|
+
${missing}
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
function markdownToHtml(markdown) {
|
|
321
|
+
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
|
|
322
|
+
const html = [];
|
|
323
|
+
let inCode = false;
|
|
324
|
+
let inList = false;
|
|
325
|
+
const closeList = () => {
|
|
326
|
+
if (inList) {
|
|
327
|
+
html.push('</ul>');
|
|
328
|
+
inList = false;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
for (const rawLine of lines) {
|
|
332
|
+
const line = rawLine.replace(/\s+$/g, '');
|
|
333
|
+
if (/^```/.test(line.trim())) {
|
|
334
|
+
if (inCode) {
|
|
335
|
+
html.push('</code></pre>');
|
|
336
|
+
inCode = false;
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
closeList();
|
|
340
|
+
html.push('<pre><code>');
|
|
341
|
+
inCode = true;
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (inCode) {
|
|
346
|
+
html.push(escapeHtml(rawLine) + '\n');
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (!line.trim()) {
|
|
350
|
+
closeList();
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
354
|
+
if (heading) {
|
|
355
|
+
closeList();
|
|
356
|
+
const level = Math.min(6, heading[1].length + 2);
|
|
357
|
+
html.push(`<h${level}>${inlineMarkdown(heading[2])}</h${level}>`);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const listItem = line.match(/^\s*[-*]\s+(.+)$/);
|
|
361
|
+
if (listItem) {
|
|
362
|
+
if (!inList) {
|
|
363
|
+
html.push('<ul>');
|
|
364
|
+
inList = true;
|
|
365
|
+
}
|
|
366
|
+
html.push(`<li>${inlineMarkdown(listItem[1])}</li>`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
closeList();
|
|
370
|
+
html.push(`<p>${inlineMarkdown(line)}</p>`);
|
|
371
|
+
}
|
|
372
|
+
closeList();
|
|
373
|
+
if (inCode)
|
|
374
|
+
html.push('</code></pre>');
|
|
375
|
+
return html.join('\n');
|
|
376
|
+
}
|
|
377
|
+
function inlineMarkdown(value) {
|
|
378
|
+
return escapeHtml(value)
|
|
379
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
380
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
381
|
+
}
|
|
382
|
+
function upsertManifestEntry(options) {
|
|
383
|
+
const existing = readManifest(options.manifestPath);
|
|
384
|
+
const artifacts = existing?.artifacts.filter(artifact => artifact.type !== options.entry.type) ?? [];
|
|
385
|
+
artifacts.push(options.entry);
|
|
386
|
+
const manifest = {
|
|
387
|
+
version: 1,
|
|
388
|
+
taskId: options.taskId ?? existing?.taskId,
|
|
389
|
+
generatedAt: new Date().toISOString(),
|
|
390
|
+
artifactDirectory: options.artifactDirectory,
|
|
391
|
+
artifacts,
|
|
392
|
+
};
|
|
393
|
+
const dir = dirname(options.manifestPath);
|
|
394
|
+
if (!existsSync(dir))
|
|
395
|
+
mkdirSync(dir, { recursive: true });
|
|
396
|
+
writeFileSync(options.manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
397
|
+
return manifest;
|
|
398
|
+
}
|
|
399
|
+
function readManifest(path) {
|
|
400
|
+
if (!existsSync(path))
|
|
401
|
+
return undefined;
|
|
402
|
+
try {
|
|
403
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
404
|
+
return {
|
|
405
|
+
version: parsed.version ?? 1,
|
|
406
|
+
taskId: parsed.taskId,
|
|
407
|
+
generatedAt: parsed.generatedAt ?? new Date(0).toISOString(),
|
|
408
|
+
artifactDirectory: parsed.artifactDirectory ?? 'artifacts',
|
|
409
|
+
artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function writeArtifactIndex(projectDir, taskDir, policy, manifest) {
|
|
417
|
+
const outputDir = join(taskDir, policy.artifactDirectory);
|
|
418
|
+
if (!existsSync(outputDir))
|
|
419
|
+
mkdirSync(outputDir, { recursive: true });
|
|
420
|
+
const indexPath = join(outputDir, 'index.html');
|
|
421
|
+
const renderer = new HTMLDocumentRenderer({
|
|
422
|
+
title: `HTML Artifacts - ${manifest.taskId ?? basename(taskDir)}`,
|
|
423
|
+
theme: policy.defaultTheme,
|
|
424
|
+
lang: 'zh',
|
|
425
|
+
interactive: false,
|
|
426
|
+
printFriendly: true,
|
|
427
|
+
});
|
|
428
|
+
const rows = manifest.artifacts.map(artifact => {
|
|
429
|
+
const href = basename(artifact.path);
|
|
430
|
+
return `<tr>
|
|
431
|
+
<td><a href="${escapeAttribute(href)}">${escapeHtml(artifact.type)}</a></td>
|
|
432
|
+
<td>${escapeHtml(artifact.title)}</td>
|
|
433
|
+
<td>${escapeHtml(artifact.gitPolicy)}</td>
|
|
434
|
+
<td>${escapeHtml(artifact.generatedAt)}</td>
|
|
435
|
+
<td>${artifact.missingSources.length ? artifact.missingSources.map(item => `<code>${escapeHtml(item)}</code>`).join(', ') : 'none'}</td>
|
|
436
|
+
</tr>`;
|
|
437
|
+
}).join('\n');
|
|
438
|
+
const html = renderer.renderReport({
|
|
439
|
+
type: 'html-artifacts',
|
|
440
|
+
title: `HTML Artifacts - ${manifest.taskId ?? basename(taskDir)}`,
|
|
441
|
+
timestamp: new Date().toISOString(),
|
|
442
|
+
metrics: {
|
|
443
|
+
artifacts: manifest.artifacts.length,
|
|
444
|
+
policy: policy.defaultGitPolicy,
|
|
445
|
+
},
|
|
446
|
+
sections: [{
|
|
447
|
+
heading: 'Artifact Index',
|
|
448
|
+
content: `<table>
|
|
449
|
+
<thead><tr><th>Type</th><th>Title</th><th>Git Policy</th><th>Generated</th><th>Missing Sources</th></tr></thead>
|
|
450
|
+
<tbody>${rows}</tbody>
|
|
451
|
+
</table>`,
|
|
452
|
+
}],
|
|
453
|
+
});
|
|
454
|
+
writeFileSync(indexPath, html, 'utf-8');
|
|
455
|
+
return indexPath;
|
|
456
|
+
}
|
|
457
|
+
function checkHtmlSafety(html, path, policy) {
|
|
458
|
+
const findings = [];
|
|
459
|
+
if (!policy.safety.allowRemoteScripts && /<script\b[^>]*\bsrc=["']https?:/i.test(html)) {
|
|
460
|
+
findings.push({
|
|
461
|
+
severity: 'fail',
|
|
462
|
+
code: 'remote-script',
|
|
463
|
+
path,
|
|
464
|
+
message: 'HTML artifact references a remote script.',
|
|
465
|
+
fix: 'Use self-contained HTML or a reviewed local asset.',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (!policy.safety.allowRemoteStyles && (/<link\b[^>]*\bhref=["']https?:/i.test(html) || /@import\s+url\(["']?https?:/i.test(html))) {
|
|
469
|
+
findings.push({
|
|
470
|
+
severity: 'fail',
|
|
471
|
+
code: 'remote-style',
|
|
472
|
+
path,
|
|
473
|
+
message: 'HTML artifact references a remote stylesheet.',
|
|
474
|
+
fix: 'Inline safe CSS or use a reviewed local asset.',
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (policy.safety.detectSecrets && containsSecretLikeValue(html)) {
|
|
478
|
+
findings.push({
|
|
479
|
+
severity: 'fail',
|
|
480
|
+
code: 'secret-like-content',
|
|
481
|
+
path,
|
|
482
|
+
message: 'HTML artifact appears to contain a credential-like value.',
|
|
483
|
+
fix: 'Regenerate after redacting tokens, cookies, credentials, and API keys from the source artifacts.',
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return findings;
|
|
487
|
+
}
|
|
488
|
+
function checkSourceFreshness(projectDir, htmlPath, artifact) {
|
|
489
|
+
const findings = [];
|
|
490
|
+
const htmlMtime = statSync(htmlPath).mtime.getTime();
|
|
491
|
+
const taskDir = dirname(dirname(htmlPath));
|
|
492
|
+
for (const sourcePath of artifact.sourcePaths) {
|
|
493
|
+
const absoluteSourcePath = resolve(taskDir, sourcePath);
|
|
494
|
+
if (!absoluteSourcePath.startsWith(taskDir) || !existsSync(absoluteSourcePath)) {
|
|
495
|
+
findings.push({
|
|
496
|
+
severity: 'warn',
|
|
497
|
+
code: 'missing-source',
|
|
498
|
+
path: sourcePath,
|
|
499
|
+
message: `Source artifact for ${artifact.type} is missing.`,
|
|
500
|
+
fix: 'Restore the source Markdown artifact or re-render with an explicit --source list.',
|
|
501
|
+
});
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (statSync(absoluteSourcePath).mtime.getTime() > htmlMtime) {
|
|
505
|
+
findings.push({
|
|
506
|
+
severity: 'warn',
|
|
507
|
+
code: 'stale-html-artifact',
|
|
508
|
+
path: normalizePath(relative(projectDir, htmlPath)),
|
|
509
|
+
message: `${artifact.type} is older than source ${sourcePath}.`,
|
|
510
|
+
fix: `Run scale artifact render --type ${artifact.type} for this task.`,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return findings;
|
|
515
|
+
}
|
|
516
|
+
function htmlArtifactSettlementMarkdown(taskId, doctor) {
|
|
517
|
+
const findings = doctor.findings.length
|
|
518
|
+
? doctor.findings.map(finding => `| ${finding.severity.toUpperCase()} | ${finding.code} | ${escapeCell(finding.path ?? '')} | ${escapeCell(finding.message)} |`).join('\n')
|
|
519
|
+
: '| OK | no-findings | | No HTML artifact findings. |';
|
|
520
|
+
const artifacts = doctor.artifacts.length
|
|
521
|
+
? doctor.artifacts.map(artifact => `| ${artifact.type} | ${escapeCell(artifact.path)} | ${artifact.sourcePaths.length} | ${artifact.missingSources.length} | ${artifact.gitPolicy} |`).join('\n')
|
|
522
|
+
: '| none | | 0 | 0 | |';
|
|
523
|
+
return `# HTML Artifacts
|
|
524
|
+
|
|
525
|
+
Task: ${taskId ?? 'unspecified'}
|
|
526
|
+
Status: ${doctor.ok ? 'passed' : 'blocked'}
|
|
527
|
+
Generated: ${new Date().toISOString()}
|
|
528
|
+
|
|
529
|
+
## Artifacts
|
|
530
|
+
|
|
531
|
+
| Type | Path | Sources | Missing | Git policy |
|
|
532
|
+
| --- | --- | ---: | ---: | --- |
|
|
533
|
+
${artifacts}
|
|
534
|
+
|
|
535
|
+
## Findings
|
|
536
|
+
|
|
537
|
+
| Severity | Code | Path | Message |
|
|
538
|
+
| --- | --- | --- | --- |
|
|
539
|
+
${findings}
|
|
540
|
+
`;
|
|
541
|
+
}
|
|
542
|
+
function sourceHeading(path) {
|
|
543
|
+
return path.replace(/[/\\]/g, ' / ').replace(/\.md$/i, '');
|
|
544
|
+
}
|
|
545
|
+
function containsSecretLikeValue(text) {
|
|
546
|
+
return /(authorization\s*:\s*bearer\s+)[A-Za-z0-9._-]{16,}/i.test(text)
|
|
547
|
+
|| /\b(password|passwd|pwd|token|secret|credential|api[_-]?key|private[_-]?key)\s*[:=]\s*["']?[A-Za-z0-9._/-]{16,}/i.test(text);
|
|
548
|
+
}
|
|
549
|
+
function escapeCell(value) {
|
|
550
|
+
return value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
551
|
+
}
|
|
552
|
+
function escapeAttribute(value) {
|
|
553
|
+
return escapeHtml(value).replace(/'/g, ''');
|
|
554
|
+
}
|
|
555
|
+
function escapeHtml(value) {
|
|
556
|
+
return value
|
|
557
|
+
.replace(/&/g, '&')
|
|
558
|
+
.replace(/</g, '<')
|
|
559
|
+
.replace(/>/g, '>')
|
|
560
|
+
.replace(/"/g, '"');
|
|
561
|
+
}
|
|
562
|
+
function normalizePath(path) {
|
|
563
|
+
return path.split(/[/\\]+/).filter(Boolean).join('/');
|
|
564
|
+
}
|
|
565
|
+
export function listExistingHtmlArtifacts(options = {}) {
|
|
566
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
567
|
+
const policy = loadHtmlArtifactPolicy(projectDir, options.scaleDir ?? '.scale');
|
|
568
|
+
const taskDir = resolveTaskDir(projectDir, options.taskId, options.artifactDir);
|
|
569
|
+
const outputDir = join(taskDir, policy.artifactDirectory);
|
|
570
|
+
if (!existsSync(outputDir))
|
|
571
|
+
return [];
|
|
572
|
+
return readdirSync(outputDir)
|
|
573
|
+
.filter(file => file.endsWith('.html'))
|
|
574
|
+
.map(file => normalizePath(relative(projectDir, join(outputDir, file))));
|
|
575
|
+
}
|
|
576
|
+
//# sourceMappingURL=HTMLArtifactLayer.js.map
|