lazycodex-ai 5.0.0-beta.89 → 5.0.0-beta.90
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.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh-cn.md +1 -1
- package/dist/cli/config-manager/refresh-opencode-plugin-sandbox.d.ts +31 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +11 -0
- package/dist/cli/index.js +1661 -859
- package/dist/cli/install-native/index.d.ts +9 -1
- package/dist/cli/install-native/legacy-omo-bin.d.ts +27 -0
- package/dist/cli/install-native/offer-native-setup.d.ts +31 -0
- package/dist/cli/install-native/omo-bin-test-fixtures.d.ts +35 -0
- package/dist/cli/install-native/plan.d.ts +8 -0
- package/dist/cli/install-native/repair-legacy-omo-bin.d.ts +26 -0
- package/dist/cli/install-native/run-native-install.d.ts +11 -1
- package/dist/cli/install-native/verify-omo-command.d.ts +22 -0
- package/dist/cli/native-edition-hint.d.ts +2 -2
- package/dist/cli-node/index.js +1661 -859
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +5 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +7 -3
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +7 -3
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +2 -2
- package/packages/omo-codex/plugin/skills/browser/SKILL.md +8 -1
- package/packages/omo-codex/plugin/skills/browser/references/install.md +34 -5
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +2 -2
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +3 -3
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +14 -3
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +13 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +7 -3
- package/packages/omo-codex/plugin/skills/ulw-research/AGENTS.md +63 -0
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +37 -25
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +187 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +66 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/cli-support.mjs +102 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +162 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/css-lite.mjs +204 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/design-spec.mjs +161 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/entities.mjs +39 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/entry-guard.mjs +21 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/format-extract.mjs +229 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-figures.mjs +173 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-layout.mjs +146 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-static.mjs +167 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-structure.mjs +118 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/gates-text.mjs +190 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/html-lite.mjs +239 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/layout-probe.mjs +83 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/outcome.mjs +250 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/report-tools.mjs +61 -0
- package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +3 -1
- package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +11 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -2
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +20 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/prompts-core/prompts/ultrawork/codex.md +7 -3
- package/packages/shared-skills/skills/browser/SKILL.md +8 -1
- package/packages/shared-skills/skills/browser/references/install.md +34 -5
- package/packages/shared-skills/skills/browser/runtime/omowright/index.js +2 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +3 -3
- package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +14 -3
- package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +13 -1
- package/packages/shared-skills/skills/ulw-research/AGENTS.md +63 -0
- package/packages/shared-skills/skills/ulw-research/SKILL.md +38 -26
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +187 -0
- package/packages/shared-skills/skills/ulw-research/references/report-gates.md +66 -0
- package/packages/shared-skills/skills/ulw-research/scripts/cli-support.mjs +102 -0
- package/packages/shared-skills/skills/ulw-research/scripts/cli-support.test.ts +88 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +162 -0
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +177 -0
- package/packages/shared-skills/skills/ulw-research/scripts/css-lite.mjs +204 -0
- package/packages/shared-skills/skills/ulw-research/scripts/css-lite.test.ts +159 -0
- package/packages/shared-skills/skills/ulw-research/scripts/design-spec.mjs +161 -0
- package/packages/shared-skills/skills/ulw-research/scripts/design-spec.test.ts +162 -0
- package/packages/shared-skills/skills/ulw-research/scripts/entities.mjs +39 -0
- package/packages/shared-skills/skills/ulw-research/scripts/entry-guard.mjs +21 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract.mjs +229 -0
- package/packages/shared-skills/skills/ulw-research/scripts/format-extract.test.ts +243 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-figures.mjs +173 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.mjs +146 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-layout.test.ts +236 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.mjs +167 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-static.test.ts +265 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-structure.mjs +118 -0
- package/packages/shared-skills/skills/ulw-research/scripts/gates-text.mjs +190 -0
- package/packages/shared-skills/skills/ulw-research/scripts/glossary-vocabulary.test.ts +43 -0
- package/packages/shared-skills/skills/ulw-research/scripts/html-lite.mjs +239 -0
- package/packages/shared-skills/skills/ulw-research/scripts/html-lite.test.ts +191 -0
- package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.mjs +83 -0
- package/packages/shared-skills/skills/ulw-research/scripts/layout-probe.test.ts +89 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.mjs +250 -0
- package/packages/shared-skills/skills/ulw-research/scripts/outcome.test.ts +281 -0
- package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
- package/packages/shared-skills/skills/ulw-research/scripts/repair-tracker.test.ts +246 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.mjs +61 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +213 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/assets/figure-1.svg +1 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/bad-report.html +44 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/boxes.json +90 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/design-spec.md +29 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/good-report.html +57 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference-report.html +86 -0
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/reference.md +23 -0
- package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +3 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// outcome.mjs - the per-run outcome manifest (`outcome.json`) and the closing briefing.
|
|
2
|
+
// Manifest functions are pure (they return a new manifest); the briefing reads only the
|
|
3
|
+
// manifest, the sources-ledger text, and an injected `now`. node: builtins only.
|
|
4
|
+
|
|
5
|
+
import * as nodeFs from "node:fs"
|
|
6
|
+
import { basename, dirname, join } from "node:path"
|
|
7
|
+
|
|
8
|
+
import { DELIVERABLE_FORMATS, DELIVERABLE_STATUSES, GATE_NAMES, GATE_STATUSES, LANES } from "./contracts.mjs"
|
|
9
|
+
import { MANIFEST_CODES, SCHEMA_VERSION, validateOutcomeManifest } from "./contracts.mjs"
|
|
10
|
+
|
|
11
|
+
export const MANIFEST_FILENAME = "outcome.json"
|
|
12
|
+
const MISSING = MANIFEST_CODES[0]
|
|
13
|
+
const SESSION_STAMP = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/
|
|
14
|
+
const SOURCE_LINE = /^\[(S|Source )\d+\]/
|
|
15
|
+
const URL_PATTERN = /https?:\/\/[^\s)<>\]"'`]+/g
|
|
16
|
+
const DRAFT_MARKER = "STATUS: draft"
|
|
17
|
+
const DRAFT_SCAN_LINES = 5
|
|
18
|
+
const REASON_REQUIRED = new Set(["blocked_capability", "skipped", "failed"])
|
|
19
|
+
|
|
20
|
+
export class OutcomeError extends Error {
|
|
21
|
+
/** @param {string} code @param {string} message @param {readonly string[]} [allowed] */
|
|
22
|
+
constructor(code, message, allowed) {
|
|
23
|
+
super(message)
|
|
24
|
+
this.name = "OutcomeError"
|
|
25
|
+
this.code = code
|
|
26
|
+
if (allowed) this.allowed = [...allowed]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertOneOf(code, label, value, allowed) {
|
|
31
|
+
if (!allowed.includes(value)) {
|
|
32
|
+
throw new OutcomeError(code, `unknown ${label} ${JSON.stringify(value)}; allowed: ${allowed.join(", ")}`, allowed)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const clone = (manifest) => structuredClone(manifest)
|
|
37
|
+
const isMissing = (error) => error?.code === "ENOENT" || error?.code === "ENOTDIR"
|
|
38
|
+
|
|
39
|
+
function stamp(now) {
|
|
40
|
+
return (now ?? new Date()).toISOString()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Local wall-clock start time from a `YYYYMMDD-HHMMSS` session dir basename. */
|
|
44
|
+
export function parseSessionStart(sessionDir) {
|
|
45
|
+
const match = SESSION_STAMP.exec(basename(sessionDir))
|
|
46
|
+
if (!match) throw new OutcomeError("invalid_session_dir", `session dir basename must be YYYYMMDD-HHMMSS: ${sessionDir}`)
|
|
47
|
+
const [y, mo, d, h, mi, s] = match.slice(1).map(Number)
|
|
48
|
+
const date = new Date(y, mo - 1, d, h, mi, s)
|
|
49
|
+
if (date.getMonth() !== mo - 1 || date.getDate() !== d || date.getHours() !== h) {
|
|
50
|
+
throw new OutcomeError("invalid_session_dir", `session dir basename is not a real local time: ${sessionDir}`)
|
|
51
|
+
}
|
|
52
|
+
return date
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {{ sessionDir: string, promised: string[], lane: string|null, interview?: object|null, now?: Date }} input */
|
|
56
|
+
export function initManifest({ sessionDir, promised, lane, interview = null, now }) {
|
|
57
|
+
const startedAt = parseSessionStart(sessionDir).toISOString()
|
|
58
|
+
if (lane !== null && lane !== undefined) assertOneOf("unknown_lane", "lane", lane, LANES)
|
|
59
|
+
const formats = [...new Set(promised)]
|
|
60
|
+
for (const format of formats) assertOneOf("unknown_format", "deliverable format", format, DELIVERABLE_FORMATS)
|
|
61
|
+
const updatedAt = stamp(now)
|
|
62
|
+
return {
|
|
63
|
+
schemaVersion: SCHEMA_VERSION,
|
|
64
|
+
sessionDir,
|
|
65
|
+
startedAt,
|
|
66
|
+
finishedAt: null,
|
|
67
|
+
elapsedMinutes: null,
|
|
68
|
+
lane: lane ?? null,
|
|
69
|
+
interview,
|
|
70
|
+
deliverables: formats.map((format) => ({ format, status: "pending", promised: true, updatedAt })),
|
|
71
|
+
renders: [],
|
|
72
|
+
gates: Object.fromEntries(GATE_NAMES.map((gate) => [gate, "not_run"])),
|
|
73
|
+
residualDefects: [],
|
|
74
|
+
repair: null,
|
|
75
|
+
sources: null,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set one format's row; every other row is kept as is. An unpromised format gets a new
|
|
81
|
+
* row with promised=false. The row's detail fields are replaced by the ones given.
|
|
82
|
+
* @param {{ path?: string, reason?: string, pages?: number, bytes?: number, now?: Date }} [detail]
|
|
83
|
+
*/
|
|
84
|
+
export function setDeliverable(manifest, format, status, detail = {}) {
|
|
85
|
+
assertOneOf("unknown_format", "deliverable format", format, DELIVERABLE_FORMATS)
|
|
86
|
+
assertOneOf("unknown_status", "deliverable status", status, DELIVERABLE_STATUSES)
|
|
87
|
+
const next = clone(manifest)
|
|
88
|
+
const index = next.deliverables.findIndex((row) => row.format === format)
|
|
89
|
+
const row = { format, status, promised: index >= 0 ? next.deliverables[index].promised : false, updatedAt: stamp(detail.now) }
|
|
90
|
+
for (const key of ["path", "reason", "pages", "bytes"]) {
|
|
91
|
+
if (detail[key] !== undefined) row[key] = detail[key]
|
|
92
|
+
}
|
|
93
|
+
if (index >= 0) next.deliverables[index] = row
|
|
94
|
+
else next.deliverables.push(row)
|
|
95
|
+
return next
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function setGate(manifest, gate, status) {
|
|
99
|
+
assertOneOf("unknown_gate", "gate", gate, GATE_NAMES)
|
|
100
|
+
assertOneOf("unknown_gate_status", "gate status", status, GATE_STATUSES)
|
|
101
|
+
const next = clone(manifest)
|
|
102
|
+
next.gates[gate] = status
|
|
103
|
+
return next
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function addRender(manifest, path, page) {
|
|
107
|
+
if (typeof path !== "string" || path.length === 0) throw new OutcomeError("invalid_render", "render path must be a non-empty string")
|
|
108
|
+
if (!Number.isInteger(page) || page < 1) throw new OutcomeError("invalid_render", `render page must be an integer >= 1, got ${page}`)
|
|
109
|
+
const next = clone(manifest)
|
|
110
|
+
if (!next.renders.some((render) => render.path === path && render.page === page)) next.renders.push({ path, page })
|
|
111
|
+
return next
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function fileSize(fs, path) {
|
|
115
|
+
try {
|
|
116
|
+
const stat = fs.statSync(path)
|
|
117
|
+
return stat.isFile() ? stat.size : null
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (isMissing(error)) return null
|
|
120
|
+
throw error
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {object} manifest
|
|
126
|
+
* @param {{ statSync: typeof nodeFs.statSync }} [fs]
|
|
127
|
+
* @returns {{ ok: boolean, problems: { code: string, format: string, message: string, path?: string }[] }}
|
|
128
|
+
*/
|
|
129
|
+
export function verifyManifest(manifest, fs = nodeFs) {
|
|
130
|
+
const problems = []
|
|
131
|
+
for (const row of manifest.deliverables) {
|
|
132
|
+
const problem = (message, path) => problems.push({ code: MISSING, format: row.format, message, ...(path ? { path } : {}) })
|
|
133
|
+
if (row.status === "pending") problem(`${row.format} is still pending`)
|
|
134
|
+
else if (row.status === "delivered") {
|
|
135
|
+
if (!row.path) problem(`${row.format} is delivered without a path`)
|
|
136
|
+
else {
|
|
137
|
+
const size = fileSize(fs, row.path)
|
|
138
|
+
if (size === null) problem(`${row.format} delivered file is missing: ${row.path}`, row.path)
|
|
139
|
+
else if (size === 0) problem(`${row.format} delivered file is empty: ${row.path}`, row.path)
|
|
140
|
+
}
|
|
141
|
+
} else if (REASON_REQUIRED.has(row.status) && !row.reason) problem(`${row.format} is ${row.status} without a reason`)
|
|
142
|
+
}
|
|
143
|
+
return { ok: problems.length === 0, problems }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* none = missing or empty file; partial = `STATUS: draft` in the first 5 lines, or the
|
|
148
|
+
* manifest (given, else a sibling outcome.json) still has a pending row; else complete.
|
|
149
|
+
* @returns {"none"|"partial"|"complete"}
|
|
150
|
+
*/
|
|
151
|
+
export function deliverableState(deliverablePath, manifestOrNull) {
|
|
152
|
+
let text
|
|
153
|
+
try {
|
|
154
|
+
text = nodeFs.readFileSync(deliverablePath, "utf8")
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (isMissing(error)) return "none"
|
|
157
|
+
throw error
|
|
158
|
+
}
|
|
159
|
+
if (text.trim() === "") return "none"
|
|
160
|
+
if (text.split(/\r?\n/, DRAFT_SCAN_LINES).some((line) => line.includes(DRAFT_MARKER))) return "partial"
|
|
161
|
+
const sibling = join(dirname(deliverablePath), MANIFEST_FILENAME)
|
|
162
|
+
const manifest = manifestOrNull ?? (nodeFs.existsSync(sibling) ? readManifest(sibling) : null)
|
|
163
|
+
if (manifest && manifest.deliverables.some((row) => row.status === "pending")) return "partial"
|
|
164
|
+
return "complete"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Hostname without a leading `www.`; null for a string URL cannot parse (not a source host). */
|
|
168
|
+
function hostOf(url) {
|
|
169
|
+
return URL.canParse(url) ? new URL(url).hostname.toLowerCase().replace(/^www\./, "") : null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function repairSummary(repair) {
|
|
173
|
+
if (!repair) return null
|
|
174
|
+
const attempts = Array.isArray(repair.attempts) ? repair.attempts.length : repair.attempts ?? null
|
|
175
|
+
return { attempts, action: repair.decision?.action ?? repair.action ?? null, reason: repair.decision?.reason ?? repair.reason ?? null }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The briefing numbers, from the manifest + ledger text + injected `now` only. */
|
|
179
|
+
export function briefingFacts(manifest, sourcesLedgerText, now) {
|
|
180
|
+
const sourceLines = String(sourcesLedgerText ?? "").split(/\r?\n/).filter((line) => SOURCE_LINE.test(line))
|
|
181
|
+
const hosts = new Set()
|
|
182
|
+
for (const line of sourceLines) {
|
|
183
|
+
for (const url of line.match(URL_PATTERN) ?? []) {
|
|
184
|
+
const host = hostOf(url)
|
|
185
|
+
if (host) hosts.add(host)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const elapsedMs = now.getTime() - new Date(manifest.startedAt).getTime()
|
|
189
|
+
return {
|
|
190
|
+
sources: sourceLines.length,
|
|
191
|
+
domains: hosts.size,
|
|
192
|
+
hosts: [...hosts].sort(),
|
|
193
|
+
minutes: Math.max(0, Math.round(elapsedMs / 60000)),
|
|
194
|
+
deliverables: manifest.deliverables.map((row) => ({ format: row.format, status: row.status, reason: row.reason ?? null, path: row.path ?? null })),
|
|
195
|
+
gates: { ...manifest.gates },
|
|
196
|
+
residualDefects: manifest.residualDefects.length,
|
|
197
|
+
repair: repairSummary(manifest.repair),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The closing briefing block as plain text. */
|
|
202
|
+
export function buildBriefing(manifest, sourcesLedgerText, now) {
|
|
203
|
+
const facts = briefingFacts(manifest, sourcesLedgerText, now)
|
|
204
|
+
const repair = facts.repair
|
|
205
|
+
? `${facts.repair.attempts ?? "?"} attempt(s), last decision ${facts.repair.action ?? "?"} (${facts.repair.reason ?? "?"})`
|
|
206
|
+
: "not run"
|
|
207
|
+
return [
|
|
208
|
+
"Closing briefing",
|
|
209
|
+
`- Sources: ${facts.sources} from ${facts.domains} distinct domain(s), counted from the sources ledger.`,
|
|
210
|
+
`- Elapsed: ${facts.minutes} minute(s) since ${manifest.startedAt}.`,
|
|
211
|
+
"- Deliverables:",
|
|
212
|
+
" | format | status | detail |",
|
|
213
|
+
" |---|---|---|",
|
|
214
|
+
...facts.deliverables.map((row) => ` | ${row.format} | ${row.status} | ${row.reason ?? row.path ?? ""} |`),
|
|
215
|
+
`- Gates: ${GATE_NAMES.map((gate) => `${gate} ${facts.gates[gate]}`).join(", ")}.`,
|
|
216
|
+
`- Residual defects: ${facts.residualDefects}.`,
|
|
217
|
+
`- Repair: ${repair}.`,
|
|
218
|
+
].join("\n")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function assertValid(manifest, where) {
|
|
222
|
+
const { ok, errors } = validateOutcomeManifest(manifest)
|
|
223
|
+
if (!ok) throw new OutcomeError("invalid_manifest", `${where}: ${errors.join("; ")}`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function readManifest(file) {
|
|
227
|
+
let manifest
|
|
228
|
+
try {
|
|
229
|
+
manifest = JSON.parse(nodeFs.readFileSync(file, "utf8"))
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error instanceof SyntaxError) throw new OutcomeError("invalid_manifest", `${file}: not valid JSON (${error.message})`)
|
|
232
|
+
throw error
|
|
233
|
+
}
|
|
234
|
+
assertValid(manifest, file)
|
|
235
|
+
return manifest
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Validate, then write atomically (temp file in the same dir + rename). */
|
|
239
|
+
export function writeManifest(file, manifest) {
|
|
240
|
+
assertValid(manifest, file)
|
|
241
|
+
const temp = join(dirname(file), `.${basename(file)}.${process.pid}.${Date.now()}.tmp`)
|
|
242
|
+
try {
|
|
243
|
+
nodeFs.writeFileSync(temp, `${JSON.stringify(manifest, null, "\t")}\n`, "utf8")
|
|
244
|
+
nodeFs.renameSync(temp, file)
|
|
245
|
+
} catch (error) {
|
|
246
|
+
nodeFs.rmSync(temp, { force: true })
|
|
247
|
+
throw error
|
|
248
|
+
}
|
|
249
|
+
return file
|
|
250
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
import { DELIVERABLE_FORMATS, GATE_NAMES, validateOutcomeManifest } from "./contracts.mjs"
|
|
7
|
+
import {
|
|
8
|
+
MANIFEST_FILENAME,
|
|
9
|
+
OutcomeError,
|
|
10
|
+
addRender,
|
|
11
|
+
briefingFacts,
|
|
12
|
+
buildBriefing,
|
|
13
|
+
deliverableState,
|
|
14
|
+
initManifest,
|
|
15
|
+
readManifest,
|
|
16
|
+
setDeliverable,
|
|
17
|
+
setGate,
|
|
18
|
+
verifyManifest,
|
|
19
|
+
writeManifest,
|
|
20
|
+
} from "./outcome.mjs"
|
|
21
|
+
|
|
22
|
+
const STAMP = "20260101-120000"
|
|
23
|
+
const START_LOCAL = new Date(2026, 0, 1, 12, 0, 0)
|
|
24
|
+
|
|
25
|
+
function sessionDir(): string {
|
|
26
|
+
const dir = join(mkdtempSync(join(tmpdir(), "ulw-outcome-")), STAMP)
|
|
27
|
+
mkdirSync(dir)
|
|
28
|
+
return dir
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function oneByteFile(dir: string, name: string): string {
|
|
32
|
+
const file = join(dir, name)
|
|
33
|
+
writeFileSync(file, "x")
|
|
34
|
+
return file
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function codes(problems: { code: string }[]): string[] {
|
|
38
|
+
return problems.map((problem) => problem.code)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("initManifest", () => {
|
|
42
|
+
test("#given promised pdf and docx #when initialised #then there is one pending row per format and the schema validates", () => {
|
|
43
|
+
// given
|
|
44
|
+
const dir = sessionDir()
|
|
45
|
+
// when
|
|
46
|
+
const manifest = initManifest({ sessionDir: dir, promised: ["pdf", "docx"], lane: "no-format" })
|
|
47
|
+
// then
|
|
48
|
+
expect(manifest.deliverables.map((row) => [row.format, row.status, row.promised])).toEqual([
|
|
49
|
+
["pdf", "pending", true],
|
|
50
|
+
["docx", "pending", true],
|
|
51
|
+
])
|
|
52
|
+
expect(manifest.startedAt).toBe(START_LOCAL.toISOString())
|
|
53
|
+
expect(Object.keys(manifest.gates)).toEqual([...GATE_NAMES])
|
|
54
|
+
expect(Object.values(manifest.gates).every((status) => status === "not_run")).toBe(true)
|
|
55
|
+
expect(validateOutcomeManifest(manifest)).toEqual({ ok: true, errors: [] })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("#given a session dir without a YYYYMMDD-HHMMSS basename #when initialised #then it throws a typed error", () => {
|
|
59
|
+
// given
|
|
60
|
+
const dir = mkdtempSync(join(tmpdir(), "ulw-outcome-"))
|
|
61
|
+
// when / then
|
|
62
|
+
expect(() => initManifest({ sessionDir: dir, promised: ["pdf"], lane: null })).toThrow(OutcomeError)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe("setDeliverable and verifyManifest", () => {
|
|
67
|
+
test("#given a fresh pdf+docx manifest #when verified #then both pending rows are missing_promised_deliverable", () => {
|
|
68
|
+
// given
|
|
69
|
+
const manifest = initManifest({ sessionDir: sessionDir(), promised: ["pdf", "docx"], lane: "no-format" })
|
|
70
|
+
// when
|
|
71
|
+
const result = verifyManifest(manifest)
|
|
72
|
+
// then
|
|
73
|
+
expect(result.ok).toBe(false)
|
|
74
|
+
expect(codes(result.problems)).toEqual(["missing_promised_deliverable", "missing_promised_deliverable"])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("#given pdf delivered as a 1-byte file and docx blocked with a reason #when verified #then it passes", () => {
|
|
78
|
+
// given
|
|
79
|
+
const dir = sessionDir()
|
|
80
|
+
const initial = initManifest({ sessionDir: dir, promised: ["pdf", "docx"], lane: "no-format" })
|
|
81
|
+
// when
|
|
82
|
+
const afterPdf = setDeliverable(initial, "pdf", "delivered", { path: oneByteFile(dir, "report.pdf") })
|
|
83
|
+
const afterDocx = setDeliverable(afterPdf, "docx", "blocked_capability", { reason: "no docx renderer" })
|
|
84
|
+
// then
|
|
85
|
+
expect(afterPdf.deliverables.find((row) => row.format === "docx")?.status).toBe("pending")
|
|
86
|
+
expect(codes(verifyManifest(afterPdf).problems)).toEqual(["missing_promised_deliverable"])
|
|
87
|
+
expect(verifyManifest(afterDocx)).toEqual({ ok: true, problems: [] })
|
|
88
|
+
expect(initial.deliverables.every((row) => row.status === "pending")).toBe(true)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test("#given a delivered row whose file is missing or empty #when verified #then each path is named in problems", () => {
|
|
92
|
+
// given
|
|
93
|
+
const dir = sessionDir()
|
|
94
|
+
const missing = join(dir, "gone.pdf")
|
|
95
|
+
const empty = join(dir, "empty.html")
|
|
96
|
+
writeFileSync(empty, "")
|
|
97
|
+
let manifest = initManifest({ sessionDir: dir, promised: ["pdf", "html"], lane: "no-format" })
|
|
98
|
+
manifest = setDeliverable(manifest, "pdf", "delivered", { path: missing })
|
|
99
|
+
manifest = setDeliverable(manifest, "html", "delivered", { path: empty })
|
|
100
|
+
// when
|
|
101
|
+
const result = verifyManifest(manifest)
|
|
102
|
+
// then
|
|
103
|
+
expect(result.ok).toBe(false)
|
|
104
|
+
expect(result.problems.map((problem) => problem.path)).toEqual([missing, empty])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test("#given a skipped row without a reason #when verified #then it is a problem", () => {
|
|
108
|
+
// given
|
|
109
|
+
const base = initManifest({ sessionDir: sessionDir(), promised: ["md"], lane: null })
|
|
110
|
+
// when
|
|
111
|
+
const result = verifyManifest(setDeliverable(base, "md", "skipped", {}))
|
|
112
|
+
// then
|
|
113
|
+
expect(codes(result.problems)).toEqual(["missing_promised_deliverable"])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("#given an unpromised format #when set #then a non-promised row is appended and the promised rows survive", () => {
|
|
117
|
+
// given
|
|
118
|
+
const dir = sessionDir()
|
|
119
|
+
const base = initManifest({ sessionDir: dir, promised: ["pdf"], lane: null })
|
|
120
|
+
// when
|
|
121
|
+
const next = setDeliverable(base, "html", "delivered", { path: oneByteFile(dir, "r.html"), bytes: 1, pages: 1 })
|
|
122
|
+
// then
|
|
123
|
+
expect(next.deliverables.map((row) => [row.format, row.status, row.promised])).toEqual([
|
|
124
|
+
["pdf", "pending", true],
|
|
125
|
+
["html", "delivered", false],
|
|
126
|
+
])
|
|
127
|
+
expect(validateOutcomeManifest(next).ok).toBe(true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("#given an unknown format #when set #then it throws a typed error naming the allowed list", () => {
|
|
131
|
+
// given
|
|
132
|
+
const base = initManifest({ sessionDir: sessionDir(), promised: ["pdf"], lane: null })
|
|
133
|
+
// when
|
|
134
|
+
let caught: unknown
|
|
135
|
+
try {
|
|
136
|
+
setDeliverable(base, "epub", "delivered", { path: "x" })
|
|
137
|
+
} catch (error) {
|
|
138
|
+
caught = error
|
|
139
|
+
}
|
|
140
|
+
// then
|
|
141
|
+
expect(caught).toBeInstanceOf(OutcomeError)
|
|
142
|
+
const error = caught as OutcomeError
|
|
143
|
+
expect(error.code).toBe("unknown_format")
|
|
144
|
+
expect(error.allowed).toEqual([...DELIVERABLE_FORMATS])
|
|
145
|
+
for (const format of DELIVERABLE_FORMATS) expect(error.message).toContain(format)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe("setGate and addRender", () => {
|
|
150
|
+
test("#given a manifest #when a gate and renders are recorded #then they land without mutating the input", () => {
|
|
151
|
+
// given
|
|
152
|
+
const base = initManifest({ sessionDir: sessionDir(), promised: ["pdf"], lane: null })
|
|
153
|
+
// when
|
|
154
|
+
const gated = setGate(base, "static", "pass")
|
|
155
|
+
const rendered = addRender(addRender(addRender(gated, "p1.png", 1), "p2.png", 2), "p1.png", 1)
|
|
156
|
+
// then
|
|
157
|
+
expect(base.gates.static).toBe("not_run")
|
|
158
|
+
expect(rendered.gates.static).toBe("pass")
|
|
159
|
+
expect(rendered.renders).toEqual([
|
|
160
|
+
{ path: "p1.png", page: 1 },
|
|
161
|
+
{ path: "p2.png", page: 2 },
|
|
162
|
+
])
|
|
163
|
+
expect(() => setGate(base, "spelling", "pass")).toThrow(OutcomeError)
|
|
164
|
+
expect(() => setGate(base, "static", "ok")).toThrow(OutcomeError)
|
|
165
|
+
expect(() => addRender(base, "p0.png", 0)).toThrow(OutcomeError)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe("deliverableState", () => {
|
|
170
|
+
test("#given the draft marker, no marker, and a sibling manifest #when read #then state follows the file and the manifest", () => {
|
|
171
|
+
// given
|
|
172
|
+
const dir = sessionDir()
|
|
173
|
+
const report = join(dir, "report.md")
|
|
174
|
+
// when / then
|
|
175
|
+
expect(deliverableState(report, null)).toBe("none")
|
|
176
|
+
writeFileSync(report, "")
|
|
177
|
+
expect(deliverableState(report, null)).toBe("none")
|
|
178
|
+
writeFileSync(report, "# Report\nSTATUS: draft \u2014 3 sections open\n\nbody\n")
|
|
179
|
+
expect(deliverableState(report, null)).toBe("partial")
|
|
180
|
+
writeFileSync(report, "# Report\n\nbody\n")
|
|
181
|
+
expect(deliverableState(report, null)).toBe("complete")
|
|
182
|
+
const pending = initManifest({ sessionDir: dir, promised: ["md"], lane: null })
|
|
183
|
+
writeManifest(join(dir, MANIFEST_FILENAME), pending)
|
|
184
|
+
expect(deliverableState(report, null)).toBe("partial")
|
|
185
|
+
const done = setDeliverable(pending, "md", "delivered", { path: report })
|
|
186
|
+
expect(deliverableState(report, done)).toBe("complete")
|
|
187
|
+
writeManifest(join(dir, MANIFEST_FILENAME), done)
|
|
188
|
+
expect(deliverableState(report, null)).toBe("complete")
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe("briefingFacts and buildBriefing", () => {
|
|
193
|
+
test("#given 2 [S<n>] lines and 1 [Source 3] line on 2 hosts and now = start + 61 min #when briefed #then sources 3, domains 2, minutes 61", () => {
|
|
194
|
+
// given
|
|
195
|
+
const dir = sessionDir()
|
|
196
|
+
const manifest = setDeliverable(
|
|
197
|
+
initManifest({ sessionDir: dir, promised: ["pdf", "docx"], lane: "no-format" }),
|
|
198
|
+
"docx",
|
|
199
|
+
"skipped",
|
|
200
|
+
{ reason: "chat delivery" },
|
|
201
|
+
)
|
|
202
|
+
const ledger = [
|
|
203
|
+
"# Sources ledger",
|
|
204
|
+
"[S1] https://alpha.example/one - primary",
|
|
205
|
+
"[S2] https://www.beta.example/two",
|
|
206
|
+
"[Source 3] https://alpha.example/three",
|
|
207
|
+
"note: https://gamma.example/not-a-source",
|
|
208
|
+
"",
|
|
209
|
+
].join("\n")
|
|
210
|
+
const now = new Date(2026, 0, 1, 13, 1, 0)
|
|
211
|
+
// when
|
|
212
|
+
const facts = briefingFacts(manifest, ledger, now)
|
|
213
|
+
const text = buildBriefing(manifest, ledger, now)
|
|
214
|
+
// then
|
|
215
|
+
expect(facts.sources).toBe(3)
|
|
216
|
+
expect(facts.domains).toBe(2)
|
|
217
|
+
expect(facts.minutes).toBe(61)
|
|
218
|
+
expect(facts.deliverables).toEqual([
|
|
219
|
+
{ format: "pdf", status: "pending", reason: null, path: null },
|
|
220
|
+
{ format: "docx", status: "skipped", reason: "chat delivery", path: null },
|
|
221
|
+
])
|
|
222
|
+
expect(facts.gates).toEqual(manifest.gates)
|
|
223
|
+
expect(facts.residualDefects).toBe(0)
|
|
224
|
+
expect(facts.repair).toBeNull()
|
|
225
|
+
expect(typeof text).toBe("string")
|
|
226
|
+
for (const token of ["pdf", "docx", "chat delivery"]) expect(text).toContain(token)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe("readManifest and writeManifest", () => {
|
|
231
|
+
test("#given an existing manifest file #when overwritten #then it round-trips and leaves no temp file", () => {
|
|
232
|
+
// given
|
|
233
|
+
const dir = sessionDir()
|
|
234
|
+
const file = join(dir, MANIFEST_FILENAME)
|
|
235
|
+
const first = initManifest({ sessionDir: dir, promised: ["pdf"], lane: null })
|
|
236
|
+
writeManifest(file, first)
|
|
237
|
+
// when
|
|
238
|
+
const second = setGate(first, "visual", "fail")
|
|
239
|
+
writeManifest(file, second)
|
|
240
|
+
// then
|
|
241
|
+
expect(readManifest(file)).toEqual(second)
|
|
242
|
+
expect(readdirSync(dir)).toEqual([MANIFEST_FILENAME])
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test("#given an invalid manifest #when written or read #then it throws a typed error and writes nothing", () => {
|
|
246
|
+
// given
|
|
247
|
+
const dir = sessionDir()
|
|
248
|
+
const file = join(dir, MANIFEST_FILENAME)
|
|
249
|
+
// when / then
|
|
250
|
+
expect(() => writeManifest(file, { schemaVersion: 99 })).toThrow(OutcomeError)
|
|
251
|
+
expect(existsSync(file)).toBe(false)
|
|
252
|
+
writeFileSync(file, "{not json")
|
|
253
|
+
expect(() => readManifest(file)).toThrow(OutcomeError)
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe("round trip in a stamped session dir", () => {
|
|
258
|
+
test("#given session dir 20260101-120000 #when init, set, gate, render, verify, brief through the file #then verify passes and the briefing is computed", () => {
|
|
259
|
+
// given
|
|
260
|
+
const dir = sessionDir()
|
|
261
|
+
const file = join(dir, MANIFEST_FILENAME)
|
|
262
|
+
writeManifest(file, initManifest({ sessionDir: dir, promised: ["html", "post"], lane: "no-format" }))
|
|
263
|
+
expect(verifyManifest(readManifest(file)).ok).toBe(false)
|
|
264
|
+
// when
|
|
265
|
+
let manifest = readManifest(file)
|
|
266
|
+
manifest = setDeliverable(manifest, "html", "delivered", { path: oneByteFile(dir, "report.html"), bytes: 1 })
|
|
267
|
+
manifest = setDeliverable(manifest, "post", "skipped", { reason: "chat delivery" })
|
|
268
|
+
manifest = addRender(setGate(manifest, "static", "pass"), join(dir, "page-1.png"), 1)
|
|
269
|
+
writeManifest(file, manifest)
|
|
270
|
+
const reread = readManifest(file)
|
|
271
|
+
const ledger = join(dir, "sources-ledger.md")
|
|
272
|
+
writeFileSync(ledger, "[S1] https://a.example/\n")
|
|
273
|
+
const facts = briefingFacts(reread, readFileSync(ledger, "utf8"), new Date(2026, 0, 1, 12, 30, 0))
|
|
274
|
+
// then
|
|
275
|
+
expect(verifyManifest(reread)).toEqual({ ok: true, problems: [] })
|
|
276
|
+
expect(facts.sources).toBe(1)
|
|
277
|
+
expect(facts.minutes).toBe(30)
|
|
278
|
+
expect(facts.gates.static).toBe("pass")
|
|
279
|
+
expect(deliverableState(join(dir, "report.html"), null)).toBe("complete")
|
|
280
|
+
})
|
|
281
|
+
})
|