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,265 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
import { DEFECT_CODES, STATIC_CODES } from "./contracts.mjs"
|
|
6
|
+
import { parseLineageMode, parsePalette } from "./design-spec.mjs"
|
|
7
|
+
import { checkStatic } from "./gates-static.mjs"
|
|
8
|
+
import { parseHtml } from "./html-lite.mjs"
|
|
9
|
+
|
|
10
|
+
type Defect = { code: string; severity: string; selector: string; line: number; message: string; hint: string }
|
|
11
|
+
type Result = { defects: Defect[]; gatesRun: string[]; gatesSkipped: string[] }
|
|
12
|
+
type FakeStat = { size: number }
|
|
13
|
+
|
|
14
|
+
const FIXTURES = join(import.meta.dir, "tests", "fixtures")
|
|
15
|
+
const GOOD = readFileSync(join(FIXTURES, "good-report.html"), "utf8")
|
|
16
|
+
const BAD = readFileSync(join(FIXTURES, "bad-report.html"), "utf8")
|
|
17
|
+
const SPEC = readFileSync(join(FIXTURES, "design-spec.md"), "utf8")
|
|
18
|
+
const PALETTE = parsePalette(SPEC)
|
|
19
|
+
|
|
20
|
+
function run(html: string, options: Record<string, unknown> = {}): Result {
|
|
21
|
+
return checkStatic(parseHtml(html), {
|
|
22
|
+
palette: PALETTE,
|
|
23
|
+
lineageMode: parseLineageMode(SPEC),
|
|
24
|
+
requireSections: [],
|
|
25
|
+
baseDir: FIXTURES,
|
|
26
|
+
...options,
|
|
27
|
+
}) as Result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const codes = (result: Result) => result.defects.map((defect) => defect.code)
|
|
31
|
+
const has = (html: string, code: string, options: Record<string, unknown> = {}) => codes(run(html, options)).includes(code)
|
|
32
|
+
|
|
33
|
+
describe("checkStatic fixtures", () => {
|
|
34
|
+
test("#given the passing fixture report #when every gate runs #then no defect is raised", () => {
|
|
35
|
+
// given
|
|
36
|
+
const options = { requireSections: [/sources/i, /review/i] }
|
|
37
|
+
// when
|
|
38
|
+
const result = run(GOOD, options)
|
|
39
|
+
// then
|
|
40
|
+
expect(codes(result)).toEqual([])
|
|
41
|
+
expect(result.gatesSkipped).toEqual([])
|
|
42
|
+
expect([...result.gatesRun].sort()).toEqual([...STATIC_CODES].sort())
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("#given the failing fixture report #when every gate runs #then each static code is raised exactly once", () => {
|
|
46
|
+
// given
|
|
47
|
+
const options = { requireSections: [/risks/i] }
|
|
48
|
+
// when
|
|
49
|
+
const result = run(BAD, options)
|
|
50
|
+
// then
|
|
51
|
+
expect([...codes(result)].sort()).toEqual([...STATIC_CODES].sort())
|
|
52
|
+
expect(STATIC_CODES).toHaveLength(17)
|
|
53
|
+
expect(codes(result)).not.toContain("missing_promised_deliverable")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("#given the failing fixture report #when defects are read #then each carries contract severity, selector, line, token message and hint", () => {
|
|
57
|
+
// given
|
|
58
|
+
const result = run(BAD, { requireSections: [/risks/i] })
|
|
59
|
+
// when
|
|
60
|
+
const byCode = new Map(result.defects.map((defect) => [defect.code, defect]))
|
|
61
|
+
// then
|
|
62
|
+
for (const defect of result.defects) {
|
|
63
|
+
const contract = DEFECT_CODES[defect.code as keyof typeof DEFECT_CODES]
|
|
64
|
+
expect(defect.severity).toBe(contract.severity)
|
|
65
|
+
expect(defect.hint).toBe(contract.hint)
|
|
66
|
+
expect(typeof defect.selector).toBe("string")
|
|
67
|
+
expect(defect.selector.length).toBeGreaterThan(0)
|
|
68
|
+
expect(Number.isInteger(defect.line)).toBe(true)
|
|
69
|
+
}
|
|
70
|
+
expect(byCode.get("palette_off_token")?.message).toContain("#123456")
|
|
71
|
+
expect(byCode.get("unsourced_number")?.message).toContain("12.5%")
|
|
72
|
+
expect(byCode.get("broken_asset_reference")?.message).toContain("missing-figure.png")
|
|
73
|
+
expect(byCode.get("korean_serif_font")?.message).toContain("Nanum Myeongjo")
|
|
74
|
+
expect(byCode.get("emoji_in_prose")?.message).toContain("\u{1F680}")
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe("mutation proofs on the passing fixture", () => {
|
|
79
|
+
test("#given keep-all removed #when checked #then only korean_no_keep_all is raised", () => {
|
|
80
|
+
// given
|
|
81
|
+
const mutated = GOOD.replace("word-break: keep-all;", "")
|
|
82
|
+
// when
|
|
83
|
+
const result = run(mutated, { requireSections: [/sources/i] })
|
|
84
|
+
// then
|
|
85
|
+
expect(codes(result)).toEqual(["korean_no_keep_all"])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test("#given the closing heading renamed #when checked #then the missing_closing_section blocker is raised", () => {
|
|
89
|
+
// given
|
|
90
|
+
const mutated = GOOD.replace("<h2>How this report was made</h2>", "<h2>Notes on the data</h2>")
|
|
91
|
+
// when
|
|
92
|
+
const result = run(mutated)
|
|
93
|
+
// then
|
|
94
|
+
const defect = result.defects.find((entry) => entry.code === "missing_closing_section")
|
|
95
|
+
expect(defect?.severity).toBe("blocker")
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test("#given the sources heading renamed #when checked #then the missing_citation_section blocker is raised", () => {
|
|
99
|
+
// given
|
|
100
|
+
const mutated = GOOD.replace("<h2>Sources</h2>", "<h2>Further reading</h2>")
|
|
101
|
+
// when
|
|
102
|
+
const result = run(mutated)
|
|
103
|
+
// then
|
|
104
|
+
const defect = result.defects.find((entry) => entry.code === "missing_citation_section")
|
|
105
|
+
expect(defect?.severity).toBe("blocker")
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test("#given no sources heading but three [S<n>] definition lines #when checked #then no citation-section blocker", () => {
|
|
109
|
+
// given
|
|
110
|
+
const html = "<h2>Notes</h2><p>[S1] First source title</p><p>[S2] Second source title</p><p>[Source 3] Third source title</p>"
|
|
111
|
+
// when
|
|
112
|
+
const found = has(html, "missing_citation_section")
|
|
113
|
+
// then
|
|
114
|
+
expect(found).toBe(false)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe("unsourced_number", () => {
|
|
119
|
+
const claim = "\uB9E4\uCD9C\uC774 12.5% \uC99D\uAC00\uD588\uB2E4"
|
|
120
|
+
|
|
121
|
+
test("#given a bare revenue claim #when checked #then unsourced_number is raised", () => {
|
|
122
|
+
// given
|
|
123
|
+
const html = `<p>${claim}</p>`
|
|
124
|
+
// when
|
|
125
|
+
const found = has(html, "unsourced_number")
|
|
126
|
+
// then
|
|
127
|
+
expect(found).toBe(true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test.each([
|
|
131
|
+
["a [S3] marker", `<p>${claim} [S3]</p>`],
|
|
132
|
+
["a MEASURED token", `<p>${claim} MEASURED</p>`],
|
|
133
|
+
["a data-lineage attribute", `<p data-lineage="DERIVED">${claim}</p>`],
|
|
134
|
+
["a #s3 anchor", `<p>${claim} <a href="#s3">3</a></p>`],
|
|
135
|
+
])("#given the claim with %s #when checked #then it is not flagged", (_label, html) => {
|
|
136
|
+
// given / when
|
|
137
|
+
const found = has(html, "unsourced_number")
|
|
138
|
+
// then
|
|
139
|
+
expect(found).toBe(false)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("#given dates, versions, figure refs and bare years only #when checked #then no claim is found", () => {
|
|
143
|
+
// given
|
|
144
|
+
const html = "<p>Released 2024-03-01 as v2.3.1 in 2019, see Figure 3 and Table 2.</p>"
|
|
145
|
+
// when
|
|
146
|
+
const found = has(html, "unsourced_number")
|
|
147
|
+
// then
|
|
148
|
+
expect(found).toBe(false)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("#given inline and section lineage modes #when the same claim is checked #then severity is major then minor", () => {
|
|
152
|
+
// given
|
|
153
|
+
const html = `<p>${claim}</p>`
|
|
154
|
+
// when
|
|
155
|
+
const inline = run(html, { lineageMode: "inline" }).defects.find((d) => d.code === "unsourced_number")
|
|
156
|
+
const section = run(html, { lineageMode: "section" }).defects.find((d) => d.code === "unsourced_number")
|
|
157
|
+
// then
|
|
158
|
+
expect(inline?.severity).toBe(DEFECT_CODES.unsourced_number.severity)
|
|
159
|
+
expect(section?.severity).toBe("minor")
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test("#given a claim inside a figure, a table and the closing section #when checked #then none is flagged", () => {
|
|
163
|
+
// given
|
|
164
|
+
const html = `<figure><p>${claim}</p><figcaption>x</figcaption></figure><table><tr><td><p>${claim}</p></td></tr></table><h2>How it was made</h2><p>${claim}</p>`
|
|
165
|
+
// when
|
|
166
|
+
const found = has(html, "unsourced_number")
|
|
167
|
+
// then
|
|
168
|
+
expect(found).toBe(false)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe("dashes and emoji", () => {
|
|
173
|
+
test("#given an en dash between years #when checked #then it is allowed", () => {
|
|
174
|
+
// given / when
|
|
175
|
+
const found = has("<p>2019\u20132024</p>", "en_dash_in_prose")
|
|
176
|
+
// then
|
|
177
|
+
expect(found).toBe(false)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test("#given an en dash between letters #when checked #then en_dash_in_prose is raised", () => {
|
|
181
|
+
// given / when
|
|
182
|
+
const found = has("<p>a\u2013b</p>", "en_dash_in_prose")
|
|
183
|
+
// then
|
|
184
|
+
expect(found).toBe(true)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test("#given em dashes in a td and in the sources section #when checked #then they are out of prose scope", () => {
|
|
188
|
+
// given
|
|
189
|
+
const html = "<table><tr><td>\u2014</td></tr></table><h2>Sources</h2><ol><li>Title \u2014 subtitle</li></ol>"
|
|
190
|
+
// when
|
|
191
|
+
const found = has(html, "em_dash_in_prose")
|
|
192
|
+
// then
|
|
193
|
+
expect(found).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test("#given a check-mark emoji in prose #when checked #then emoji_in_prose is raised", () => {
|
|
197
|
+
// given / when
|
|
198
|
+
const found = has("<p>\u2705 done</p>", "emoji_in_prose")
|
|
199
|
+
// then
|
|
200
|
+
expect(found).toBe(true)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test("#given a plain check mark in a td #when checked #then no emoji is raised", () => {
|
|
204
|
+
// given / when
|
|
205
|
+
const found = has("<table><tr><td>\u2713</td></tr></table>", "emoji_in_prose")
|
|
206
|
+
// then
|
|
207
|
+
expect(found).toBe(false)
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe("figures and charts", () => {
|
|
212
|
+
test("#given a 16px svg inside an h2 #when checked #then it is not treated as a chart", () => {
|
|
213
|
+
// given
|
|
214
|
+
const html = '<h2>Title <svg width="16" height="16" viewBox="0 0 16 16"><path d="M0 0h16"/></svg></h2>'
|
|
215
|
+
// when
|
|
216
|
+
const found = codes(run(html)).filter((code) => code.startsWith("chart_"))
|
|
217
|
+
// then
|
|
218
|
+
expect(found).toEqual([])
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test("#given a figure svg with title, three ticks and a label #when checked #then no chart defect", () => {
|
|
222
|
+
// given
|
|
223
|
+
const svg = '<svg width="400" height="200"><title>Deploys</title><text>0</text><text>5</text><text>10</text><text>Weeks</text></svg>'
|
|
224
|
+
// when
|
|
225
|
+
const found = codes(run(`<figure>${svg}<figcaption>Deploys per week</figcaption></figure>`)).filter((code) => code.startsWith("chart_"))
|
|
226
|
+
// then
|
|
227
|
+
expect(found).toEqual([])
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe("gate bookkeeping", () => {
|
|
232
|
+
test("#given a spec with an empty palette #when checked #then palette_off_token is skipped, not run", () => {
|
|
233
|
+
// given
|
|
234
|
+
const palette = parsePalette("# Design spec\n\n## Citations\n- Lineage: inline\n")
|
|
235
|
+
// when
|
|
236
|
+
const result = run('<p style="color: #123456">x</p>', { palette })
|
|
237
|
+
// then
|
|
238
|
+
expect(palette.size).toBe(0)
|
|
239
|
+
expect(result.gatesSkipped).toContain("palette_off_token")
|
|
240
|
+
expect(result.gatesRun).not.toContain("palette_off_token")
|
|
241
|
+
expect(codes(result)).not.toContain("palette_off_token")
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test("#given an injected fs with a zero-byte and a missing asset #when checked #then both are broken and remote ones are ignored", () => {
|
|
245
|
+
// given
|
|
246
|
+
const sizes = new Map<string, number>([[join("base", "empty.png"), 0], [join("base", "ok.css"), 10]])
|
|
247
|
+
const fs = {
|
|
248
|
+
statSync(file: string): FakeStat {
|
|
249
|
+
const size = sizes.get(file)
|
|
250
|
+
if (size === undefined) throw Object.assign(new Error("ENOENT"), { code: "ENOENT" })
|
|
251
|
+
return { size }
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
const html = '<link rel="stylesheet" href="ok.css"><img src="empty.png"><script src="gone.js"></script><img src="https://example.org/a.png">'
|
|
255
|
+
// when
|
|
256
|
+
const broken = run(html, { baseDir: "base", fs }).defects.filter((d) => d.code === "broken_asset_reference")
|
|
257
|
+
// then
|
|
258
|
+
expect(broken.map((d) => d.message.includes("empty.png") || d.message.includes("gone.js"))).toEqual([true, true])
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test("#given an unknown lineage mode #when checked #then it throws instead of guessing a severity", () => {
|
|
262
|
+
// given / when / then
|
|
263
|
+
expect(() => run("<p>x</p>", { lineageMode: "loose" })).toThrow(TypeError)
|
|
264
|
+
})
|
|
265
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// gates-structure.mjs - structure gates: G11 section without citation, G12 closing section,
|
|
2
|
+
// G13 citation section, G14 broken asset reference, G15 required sections.
|
|
3
|
+
//
|
|
4
|
+
// Each gate is { codes, skip?(ctx), run(ctx) -> Defect[] } over the context built by
|
|
5
|
+
// gates-static.mjs. Plain Node ESM, node: builtins and siblings only.
|
|
6
|
+
|
|
7
|
+
import { join } from "node:path"
|
|
8
|
+
|
|
9
|
+
import { proseText } from "./html-lite.mjs"
|
|
10
|
+
|
|
11
|
+
const MIN_SECTION_PROSE = 120
|
|
12
|
+
const UNCHECKED_KINDS = new Set(["closing", "sources", "toc", "appendix", "glossary"])
|
|
13
|
+
const DEFINITION_TAGS = new Set(["p", "li", "dd", "dt", "td"])
|
|
14
|
+
const DEFINITION_LINE = /^\[(S|Source\s*)(\d+)\]/i
|
|
15
|
+
const MIN_DEFINITIONS = 3
|
|
16
|
+
const NOT_RELATIVE = /^(?:[a-z][a-z\d+.-]*:|\/\/|\/|#)/i
|
|
17
|
+
|
|
18
|
+
function headingNodes(ctx, tags) {
|
|
19
|
+
return ctx.elements.map(({ node }) => node).filter((node) => tags.includes(node.tag))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Local references an asset gate checks: img src, source srcset, stylesheet href, script src. */
|
|
23
|
+
function assetReferences(ctx) {
|
|
24
|
+
const refs = []
|
|
25
|
+
for (const node of ctx.all) {
|
|
26
|
+
const { attrs } = node
|
|
27
|
+
if (node.tag === "img" && attrs.src) refs.push({ node, url: attrs.src })
|
|
28
|
+
else if (node.tag === "script" && attrs.src) refs.push({ node, url: attrs.src })
|
|
29
|
+
else if (node.tag === "link" && attrs.href && /(?:^|\s)stylesheet(?:\s|$)/i.test(attrs.rel ?? "")) refs.push({ node, url: attrs.href })
|
|
30
|
+
else if (node.tag === "source" && attrs.srcset) {
|
|
31
|
+
for (const candidate of attrs.srcset.split(",")) {
|
|
32
|
+
const url = candidate.trim().split(/\s+/)[0]
|
|
33
|
+
if (url) refs.push({ node, url })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return refs.filter(({ url }) => url.trim() && !NOT_RELATIVE.test(url.trim()))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function localPath(baseDir, url) {
|
|
41
|
+
const bare = url.trim().replace(/[?#].*$/, "")
|
|
42
|
+
let decoded = bare
|
|
43
|
+
try {
|
|
44
|
+
decoded = decodeURIComponent(bare)
|
|
45
|
+
} catch {
|
|
46
|
+
// a malformed %-escape is kept verbatim; the stat below reports it as missing
|
|
47
|
+
}
|
|
48
|
+
return join(baseDir, ...decoded.split("/"))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assetProblem(fs, file) {
|
|
52
|
+
try {
|
|
53
|
+
const stat = fs.statSync(file)
|
|
54
|
+
return stat.size === 0 ? "is 0 bytes" : null
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return `is missing (${error?.code ?? "stat failed"})`
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const structureGates = [
|
|
61
|
+
{
|
|
62
|
+
codes: ["section_without_citation"],
|
|
63
|
+
run(ctx) {
|
|
64
|
+
const defects = []
|
|
65
|
+
for (const section of ctx.sections) {
|
|
66
|
+
if (UNCHECKED_KINDS.has(section.kind)) continue
|
|
67
|
+
const prose = section.prose.join(" ").replace(/\s+/g, " ").trim()
|
|
68
|
+
if (prose.length < MIN_SECTION_PROSE || section.anchors > 0 || ctx.util.citationMarker.test(prose)) continue
|
|
69
|
+
const title = proseText(section.heading)
|
|
70
|
+
defects.push(ctx.defect("section_without_citation", section.heading, `section "${title}" has ${prose.length} prose characters and no citation marker or anchor`))
|
|
71
|
+
}
|
|
72
|
+
return defects
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
codes: ["missing_closing_section"],
|
|
77
|
+
run(ctx) {
|
|
78
|
+
if (headingNodes(ctx, ["h2", "h3"]).some((node) => ctx.isClosing(proseText(node)))) return []
|
|
79
|
+
return [ctx.defect("missing_closing_section", ctx.body, "no h2/h3 heading names how the report was made (method / how)")]
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
codes: ["missing_citation_section"],
|
|
84
|
+
run(ctx) {
|
|
85
|
+
if (ctx.sections.some((section) => section.kind === "sources")) return []
|
|
86
|
+
const ids = new Set()
|
|
87
|
+
for (const { node } of ctx.elements) {
|
|
88
|
+
if (!DEFINITION_TAGS.has(node.tag)) continue
|
|
89
|
+
const match = DEFINITION_LINE.exec(proseText(node))
|
|
90
|
+
if (match) ids.add(`${match[1].trim().toLowerCase()}${match[2]}`)
|
|
91
|
+
}
|
|
92
|
+
if (ids.size >= MIN_DEFINITIONS) return []
|
|
93
|
+
return [ctx.defect("missing_citation_section", ctx.body, `no references/sources h2 and ${ids.size} [S<n>] definition line(s) (< ${MIN_DEFINITIONS})`)]
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
codes: ["broken_asset_reference"],
|
|
98
|
+
skip: (ctx) => !ctx.baseDir,
|
|
99
|
+
run(ctx) {
|
|
100
|
+
const defects = []
|
|
101
|
+
for (const { node, url } of assetReferences(ctx)) {
|
|
102
|
+
const problem = assetProblem(ctx.fs, localPath(ctx.baseDir, url))
|
|
103
|
+
if (problem) defects.push(ctx.defect("broken_asset_reference", node, `<${node.tag}> asset "${url}" ${problem}`))
|
|
104
|
+
}
|
|
105
|
+
return defects
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
codes: ["missing_required_section"],
|
|
110
|
+
skip: (ctx) => ctx.requireSections.length === 0,
|
|
111
|
+
run(ctx) {
|
|
112
|
+
const titles = headingNodes(ctx, ["h1", "h2", "h3", "h4", "h5", "h6"]).map((node) => proseText(node))
|
|
113
|
+
return ctx.requireSections
|
|
114
|
+
.filter((re) => !titles.some((title) => re.test(title)))
|
|
115
|
+
.map((re) => ctx.defect("missing_required_section", ctx.body, `no heading matches the required section ${re}`))
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// gates-text.mjs - prose gates: G1 keep-all, G3 emoji, G4 dashes, G5/G6 headings, G7 unsourced numbers.
|
|
2
|
+
//
|
|
3
|
+
// Each gate is { codes, run(ctx) -> Defect[] } over the context built by gates-static.mjs.
|
|
4
|
+
// Plain Node ESM, sibling imports only.
|
|
5
|
+
|
|
6
|
+
import { proseText } from "./html-lite.mjs"
|
|
7
|
+
|
|
8
|
+
const HANGUL = /[\uAC00-\uD7A3]/g
|
|
9
|
+
const KEEP_ALL = /word-break\s*:\s*keep-all/i
|
|
10
|
+
const MIN_HANGUL = 20
|
|
11
|
+
/** Dingbats (2700-27BF) that render as emoji without FE0F (Emoji_Presentation=Yes). */
|
|
12
|
+
const DINGBAT_EMOJI = new Set([0x2705, 0x270a, 0x270b, 0x2728, 0x274c, 0x274e, 0x2753, 0x2754, 0x2755, 0x2757, 0x2795, 0x2796, 0x2797, 0x27b0, 0x27bf])
|
|
13
|
+
const EM_DASH = "\u2014"
|
|
14
|
+
const EN_DASH = "\u2013"
|
|
15
|
+
const HEADING_NUMBER = /^\s*(?:\d+(?:[.-]\d+)*[.):]?|[IVXLC]+[.)]|[A-Z][.)])\s+/
|
|
16
|
+
const GENERIC = new Set(["overview", "introduction", "background", "summary", "conclusion", "conclusions", "results", "discussion", "methodology"])
|
|
17
|
+
const MAX_HEADING_HANGUL = 26
|
|
18
|
+
const MAX_HEADING_WORDS = 12
|
|
19
|
+
const LINEAGE_TOKEN = /\b(?:MEASURED|ASSUMED|DERIVED)\b/
|
|
20
|
+
const NUMBER_CONTAINERS = new Set(["figure", "table", "nav", "header", "footer"])
|
|
21
|
+
const MAX_UNSOURCED = 50
|
|
22
|
+
const MONTH = "(?:jan|feb|mar|apr|may|jun|jul|aug|sep|sept|oct|nov|dec)[a-z]*\\.?"
|
|
23
|
+
/** Stripped before claim detection: dates, versions, figure refs, times, bare years. */
|
|
24
|
+
const NOT_CLAIMS = [
|
|
25
|
+
/\b\d{4}[-./]\d{1,2}(?:[-./]\d{1,2})?\b/g,
|
|
26
|
+
/\b\d{2}\.\d{2}\.\d{2}\b/g,
|
|
27
|
+
/\d{2,4}\s*\uB144\s*\d{1,2}\s*\uC6D4(?:\s*\d{1,2}\s*\uC77C)?/g,
|
|
28
|
+
/\d{1,2}\s*\uC6D4(?:\s*\d{1,2}\s*\uC77C)?/g,
|
|
29
|
+
/(?:19|20)\d{2}\s*\uB144/g,
|
|
30
|
+
new RegExp(`\\b${MONTH}\\s+\\d{1,2}(?:st|nd|rd|th)?(?:,?\\s+\\d{4})?`, "gi"),
|
|
31
|
+
new RegExp(`\\b\\d{1,2}(?:st|nd|rd|th)?\\s+${MONTH}(?:\\s+\\d{4})?`, "gi"),
|
|
32
|
+
/\bv\d+(?:\.\d+)*\b|\b\d+(?:\.\d+){2,}\b/gi,
|
|
33
|
+
/\b(?:version|ver\.?|release)\s*\d+(?:\.\d+)*/gi,
|
|
34
|
+
/\b(?:fig(?:ure)?\.?|table|chart|section|step|phase|wave|part)\s*\d+(?:\.\d+)*/gi,
|
|
35
|
+
/(?:\uADF8\uB9BC|\uB3C4\uD45C|\uD45C|\uBC84\uC804)\s*\d+(?:\.\d+)*/g,
|
|
36
|
+
/\b\d{1,2}:\d{2}\b/g,
|
|
37
|
+
/\b(?:19|20)\d{2}s?\b/g,
|
|
38
|
+
]
|
|
39
|
+
const NUM = "(?<![\\p{L}\\d.,])\\d+(?:[.,]\\d+)*"
|
|
40
|
+
const UNIT =
|
|
41
|
+
"(?:%|\\u2030|\\u00D7|\\uD37C\\uC13C\\uD2B8|\\uB2EC\\uB7EC|\\uC720\\uB85C|\\uC5D4|\\uC6D0|\\uB9CC|\\uC5B5|\\uC870|\\uCC9C|\\uCD08|\\uBD84|\\uC2DC\\uAC04|\\uC77C|\\uC8FC|\\uAC1C\\uC6D4|\\uB144|\\uBA85|\\uAC74|\\uAC1C|\\uD68C|\\uBC88|\\uBC30" +
|
|
42
|
+
"|(?:percent|pp|bps?|x|k|m|b|bn|million|billion|thousand|trillion|usd|krw|eur|ms|s|secs?|seconds?|mins?|minutes?|h|hrs?|hours?|days?|weeks?|months?|years?|kb|mb|gb|tb|users?|people|employees|engineers|tokens?|requests?|stars?|downloads?|customers?)(?![\\p{L}\\d]))"
|
|
43
|
+
const CLAIMS = [
|
|
44
|
+
new RegExp(`[$\\u20AC\\u00A3\\u00A5\\u20A9]\\s?${NUM}`, "u"),
|
|
45
|
+
new RegExp(`${NUM}\\s?${UNIT}`, "iu"),
|
|
46
|
+
/(?<![\d.,])\d{1,3}(?:,\d{3})+(?![\d,])/,
|
|
47
|
+
/(?<![\d.])\d+\.\d+(?![\d.])/,
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
function snippet(text, index, span = 24) {
|
|
51
|
+
return text.slice(Math.max(0, index - span), index + span).trim()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Concatenated prose per owning element, in document order; `keep` filters text entries. */
|
|
55
|
+
function ownerTexts(ctx, keep) {
|
|
56
|
+
const byOwner = new Map()
|
|
57
|
+
for (const entry of ctx.texts) {
|
|
58
|
+
if (!keep(entry)) continue
|
|
59
|
+
byOwner.set(entry.owner, (byOwner.get(entry.owner) ?? "") + entry.node.text)
|
|
60
|
+
}
|
|
61
|
+
return [...byOwner].map(([owner, text]) => ({ owner, text: text.replace(/\s+/g, " ") }))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function firstEmoji(text) {
|
|
65
|
+
const chars = [...text]
|
|
66
|
+
for (let i = 0; i < chars.length; i++) {
|
|
67
|
+
const cp = chars[i].codePointAt(0)
|
|
68
|
+
if (chars[i + 1] === "\uFE0F" && cp !== 0xfe0f) return chars[i] + chars[i + 1]
|
|
69
|
+
if ((cp >= 0x1f000 && cp <= 0x1faff) || (cp >= 0x2600 && cp <= 0x26ff) || DINGBAT_EMOJI.has(cp)) return chars[i]
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isDigit = (ch) => ch >= "0" && ch <= "9"
|
|
75
|
+
|
|
76
|
+
/** En dash allowed only when both neighbors (one optional space) are digits. */
|
|
77
|
+
function badEnDash(text) {
|
|
78
|
+
for (let at = text.indexOf(EN_DASH); at !== -1; at = text.indexOf(EN_DASH, at + 1)) {
|
|
79
|
+
const before = text[at - 1] === " " ? text[at - 2] : text[at - 1]
|
|
80
|
+
const after = text[at + 1] === " " ? text[at + 2] : text[at + 1]
|
|
81
|
+
if (!isDigit(before ?? "") || !isDigit(after ?? "")) return at
|
|
82
|
+
}
|
|
83
|
+
return -1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const stripNumbering = (text) => text.replace(HEADING_NUMBER, "").trim()
|
|
87
|
+
|
|
88
|
+
function headings(ctx, max) {
|
|
89
|
+
return ctx.elements.filter(({ node }) => /^h[1-6]$/.test(node.tag) && Number(node.tag[1]) <= max).map(({ node }) => node)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Citation markers need no stripping: an element carrying one is skipped before this runs. */
|
|
93
|
+
function claimIn(text) {
|
|
94
|
+
let rest = text
|
|
95
|
+
for (const re of NOT_CLAIMS) rest = rest.replace(re, " ")
|
|
96
|
+
for (const re of CLAIMS) {
|
|
97
|
+
const match = re.exec(rest)
|
|
98
|
+
if (match) return match[0].trim()
|
|
99
|
+
}
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function numberCandidates(ctx) {
|
|
104
|
+
const { closest, classTokens, descendants } = ctx.util
|
|
105
|
+
return ctx.elements.filter(({ node, scope }) => {
|
|
106
|
+
if (node.tag !== "p" && node.tag !== "li") return false
|
|
107
|
+
if (node.tag === "li" && descendants(node).some((child) => child.tag === "p" || child.tag === "li")) return false
|
|
108
|
+
const kind = ctx.kindOf(scope)
|
|
109
|
+
if (kind === "closing" || kind === "sources") return false
|
|
110
|
+
return !closest(node, (cur) => NUMBER_CONTAINERS.has(cur.tag) || classTokens(cur).includes("fig"))
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const textGates = [
|
|
115
|
+
{
|
|
116
|
+
codes: ["korean_no_keep_all"],
|
|
117
|
+
run(ctx) {
|
|
118
|
+
const syllables = ctx.texts.reduce((sum, entry) => sum + (entry.node.text.match(HANGUL)?.length ?? 0), 0)
|
|
119
|
+
if (syllables < MIN_HANGUL) return []
|
|
120
|
+
if ([...ctx.styleBlocks, ...ctx.styleAttrs].some((entry) => KEEP_ALL.test(entry.css))) return []
|
|
121
|
+
return [ctx.defect("korean_no_keep_all", ctx.body, `${syllables} Hangul syllables in prose and no "word-break: keep-all" in any style block or style attribute`)]
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
codes: ["emoji_in_prose"],
|
|
126
|
+
run(ctx) {
|
|
127
|
+
const defects = []
|
|
128
|
+
for (const { owner, text } of ownerTexts(ctx, () => true)) {
|
|
129
|
+
const emoji = firstEmoji(text)
|
|
130
|
+
if (emoji) defects.push(ctx.defect("emoji_in_prose", owner, `emoji "${emoji}" in <${owner.tag}>: "${snippet(text, text.indexOf(emoji))}"`))
|
|
131
|
+
}
|
|
132
|
+
return defects
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
codes: ["em_dash_in_prose", "en_dash_in_prose"],
|
|
137
|
+
run(ctx) {
|
|
138
|
+
const defects = []
|
|
139
|
+
const inScope = (entry) => !entry.cell && ctx.kindOf(entry.scope) !== "sources"
|
|
140
|
+
for (const { owner, text } of ownerTexts(ctx, inScope)) {
|
|
141
|
+
const em = text.indexOf(EM_DASH)
|
|
142
|
+
if (em !== -1) defects.push(ctx.defect("em_dash_in_prose", owner, `em dash "${EM_DASH}" in <${owner.tag}>: "${snippet(text, em)}"`))
|
|
143
|
+
const en = badEnDash(text)
|
|
144
|
+
if (en !== -1) defects.push(ctx.defect("en_dash_in_prose", owner, `en dash "${EN_DASH}" outside a numeric range in <${owner.tag}>: "${snippet(text, en)}"`))
|
|
145
|
+
}
|
|
146
|
+
return defects
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
codes: ["heading_too_long"],
|
|
151
|
+
run(ctx) {
|
|
152
|
+
const defects = []
|
|
153
|
+
for (const node of headings(ctx, 4)) {
|
|
154
|
+
const text = stripNumbering(proseText(node))
|
|
155
|
+
const hangul = /[\uAC00-\uD7A3]/.test(text)
|
|
156
|
+
const size = hangul ? [...text].length : text.split(/\s+/).filter(Boolean).length
|
|
157
|
+
if (size > (hangul ? MAX_HEADING_HANGUL : MAX_HEADING_WORDS)) {
|
|
158
|
+
defects.push(ctx.defect("heading_too_long", node, `<${node.tag}> "${text}" is ${size} ${hangul ? "characters (> 26)" : "words (> 12)"}`))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return defects
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
codes: ["heading_generic"],
|
|
166
|
+
run(ctx) {
|
|
167
|
+
const defects = []
|
|
168
|
+
for (const node of headings(ctx, 3)) {
|
|
169
|
+
const text = stripNumbering(proseText(node))
|
|
170
|
+
if (GENERIC.has(text.toLowerCase().replace(/[.:]+$/, ""))) defects.push(ctx.defect("heading_generic", node, `generic <${node.tag}> "${text}"`))
|
|
171
|
+
}
|
|
172
|
+
return defects
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
codes: ["unsourced_number"],
|
|
177
|
+
run(ctx) {
|
|
178
|
+
const defects = []
|
|
179
|
+
for (const { node } of numberCandidates(ctx)) {
|
|
180
|
+
if (defects.length >= MAX_UNSOURCED) break
|
|
181
|
+
const text = proseText(node)
|
|
182
|
+
if (LINEAGE_TOKEN.test(text) || ctx.util.hasCitation(node)) continue
|
|
183
|
+
if (node.attrs["data-lineage"] !== undefined || ctx.util.descendants(node).some((child) => child.attrs["data-lineage"] !== undefined)) continue
|
|
184
|
+
const claim = claimIn(text)
|
|
185
|
+
if (claim) defects.push(ctx.defect("unsourced_number", node, `number "${claim}" without a lineage tag or citation: "${snippet(text, text.indexOf(claim))}"`))
|
|
186
|
+
}
|
|
187
|
+
return defects
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
import { DEFECT_CODES, GATE_STATUSES, LINEAGE_MODES } from "./contracts.mjs"
|
|
6
|
+
|
|
7
|
+
describe("report-gates glossary vocabulary parity", () => {
|
|
8
|
+
test("#given the defect codes #when the glossary is loaded #then every exported code appears as a backticked table cell", () => {
|
|
9
|
+
const glossaryPath = join(import.meta.dir, "../references/report-gates.md")
|
|
10
|
+
const glossary = readFileSync(glossaryPath, "utf8")
|
|
11
|
+
|
|
12
|
+
const missing: string[] = []
|
|
13
|
+
for (const code of Object.keys(DEFECT_CODES)) {
|
|
14
|
+
if (!glossary.includes(`\`${code}\``)) {
|
|
15
|
+
missing.push(code)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
expect(missing, `Missing codes: ${missing.join(", ")}`).toEqual([])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test("#given the glossary #when scanned for backticked tokens #then no extra defect code appears that is not exported", () => {
|
|
23
|
+
const glossaryPath = join(import.meta.dir, "../references/report-gates.md")
|
|
24
|
+
const glossary = readFileSync(glossaryPath, "utf8")
|
|
25
|
+
|
|
26
|
+
const defectCodeSet = new Set(Object.keys(DEFECT_CODES))
|
|
27
|
+
const validTokens = new Set([...defectCodeSet, ...GATE_STATUSES, ...LINEAGE_MODES])
|
|
28
|
+
const backtickRegex = /`([a-z_][a-z0-9_]*)`/g
|
|
29
|
+
const extras: string[] = []
|
|
30
|
+
|
|
31
|
+
let match
|
|
32
|
+
while ((match = backtickRegex.exec(glossary)) !== null) {
|
|
33
|
+
const token = match[1]
|
|
34
|
+
// Only flag tokens that look like defect codes (underscore-separated)
|
|
35
|
+
// and aren't already exported from contracts
|
|
36
|
+
if (!validTokens.has(token) && token.includes("_") && token.length > 3) {
|
|
37
|
+
if (!extras.includes(token)) extras.push(token)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
expect(extras, `Extra codes not in contracts: ${extras.join(", ")}`).toEqual([])
|
|
42
|
+
})
|
|
43
|
+
})
|