Skip to content

Commit c369294

Browse files
the-homeless-godMarat Zimnurov
authored andcommitted
Разбор суждений попал в основную проверку, и пакет получил существующий scope
Две вещи, обе про то, что правильное решение молчало. ПЕРВОЕ. Анализ, находивший настоящие дефекты, требовал отдельного запуска. tools/ftsmap умеет разбирать утилиту от входов и точно, а не на выборке: intervals.mjs решает задачу покоординатно за линейное время и честно называет класс, который не берёт. Именно им были пойманы два дефекта в моделях учебного курса — недостижимый процентный предел и нарушение на отрицательной сумме. Но `fts check` про это молчал, и увидеть находку можно было, только зная, что надо запустить ftsmap. Теперь check говорит сам, предупреждением, а не ошибкой — модель с недостижимым свойством валидна: FTS_COVERAGE_HOLE при «сумма» ∈ (−∞, 10000) не срабатывает ни одно правило — результат остаётся начальным (0) строка 6 FTS_PROPERTY_VIOLATED свойство «Скидка ограничена» нарушается при «сумма» ∈ (−∞, 0): результат 0 против предела −200000 строка 15 FTS_PROPERTY_UNATTAINABLE предел «результат ≤ 20 % от поля «сумма»» не берётся нигде, где правила меняют результат строка 15 У каждой находки свой код, а не общий FTS_UTILITY_*: по коду машина понимает, что чинить, и это прямо нужно для связки «разработчик и ИИ». Появилось место — строка, которой у находок ftsmap не было вовсе, хотя у диагностик компилятора она есть. Появилась конкретика: не «свойство нарушается», а на каком интервале и какое число против какого. ВТОРОЕ. Пакет объявлял scope @digitable, которого в реестре не существует, — организация называется @digitable-lol. Я проверил @digitable, получил 404 и сделал вывод «организации нет», не догадавшись проверить второе имя. Имя пакета исправлено в 12 файлах, включая манифест вендорной сборки, документацию и workflow публикации. Проверено: 71 тест ядра, 405 инструментов (4 пропуска — нет dotnet и Go), сборка TypeScript без ошибок.
1 parent 0cd0e90 commit c369294

22 files changed

Lines changed: 1878 additions & 79 deletions

.github/actions/fts-check/lib/annotate.mjs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ import { formatCommand } from "./commands.mjs"
2727
* message text so the location information is not silently dropped.
2828
*/
2929
export function diagnosticToAnnotation(diagnostic, file, spot = locate(diagnostic, null, { fallback: "none" })) {
30-
const severity = diagnostic.severity === "warning" ? "warning" : "error"
30+
/* `info` — true and worth printing, but nothing is wrong; GitHub calls that
31+
level `notice`. Only an unknown severity is treated as an error. */
32+
const severity =
33+
diagnostic.severity === "warning" ? "warning" : diagnostic.severity === "info" ? "notice" : "error"
3134
const properties = {}
3235
if (file !== undefined) properties.file = file
3336

@@ -42,7 +45,10 @@ export function diagnosticToAnnotation(diagnostic, file, spot = locate(diagnosti
4245
}
4346
if (diagnostic.code) properties.title = diagnostic.code
4447

45-
const message = spot || !diagnostic.path ? diagnostic.message : `${diagnostic.message} (${diagnostic.path})`
48+
const located = spot || !diagnostic.path ? diagnostic.message : `${diagnostic.message} (${diagnostic.path})`
49+
/* A hint is the actionable half of a diagnostic; an annotation that dropped
50+
it would send the reader back to the terminal to see what to do. */
51+
const message = diagnostic.hint ? `${located}\n${diagnostic.hint}` : located
4652

4753
return formatCommand(severity, properties, message)
4854
}

.github/actions/fts-check/lib/run.mjs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,13 @@ export async function run(options) {
102102
}
103103

104104
const allDiagnostics = [...models.flatMap((m) => m.diagnostics), ...tools.flatMap((t) => t.diagnostics)]
105-
const errors = allDiagnostics.filter((d) => d.severity !== "warning").length
106-
const warnings = allDiagnostics.length - errors
105+
const warnings = allDiagnostics.filter((d) => d.severity === "warning").length
106+
/* `info` never fails a build and is not a warning either: it reports
107+
something true about the model that is not a defect — two rules that both
108+
add to the result and therefore do not care about their order. An unknown
109+
severity still counts as an error. */
110+
const notices = allDiagnostics.filter((d) => d.severity === "info").length
111+
const errors = allDiagnostics.length - warnings - notices
107112
const examplesFailed = models.reduce((sum, m) => sum + m.examplesFailed, 0)
108113

109114
const failed = errors > 0 || (failOnWarning && warnings > 0)
@@ -118,6 +123,7 @@ export async function run(options) {
118123
diagnostics: allDiagnostics.length,
119124
errors,
120125
warnings,
126+
notices,
121127
examplesFailed,
122128
},
123129
failed,

.github/actions/fts-check/lib/summaryReport.mjs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ function cell(text) {
33
return String(text).replace(/\|/g, "\\|").replace(/\r?\n/g, " ")
44
}
55

6+
/**
7+
* Counted by declared severity, and an unknown severity counts as an error:
8+
* a level this action has not heard of must not be quietly downgraded.
9+
*/
610
function countBySeverity(diagnostics) {
7-
const errors = diagnostics.filter((d) => d.severity !== "warning").length
8-
const warnings = diagnostics.length - errors
9-
return { errors, warnings }
11+
const warnings = diagnostics.filter((d) => d.severity === "warning").length
12+
const notices = diagnostics.filter((d) => d.severity === "info").length
13+
return { errors: diagnostics.length - warnings - notices, warnings, notices }
1014
}
1115

1216
function diagnosticsCell(diagnostics) {
1317
if (diagnostics.length === 0) return "0"
14-
const { errors, warnings } = countBySeverity(diagnostics)
18+
const { errors, warnings, notices } = countBySeverity(diagnostics)
1519
const parts = []
1620
if (errors > 0) parts.push(`${errors} error${errors === 1 ? "" : "s"}`)
1721
if (warnings > 0) parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`)
22+
if (notices > 0) parts.push(`${notices} notice${notices === 1 ? "" : "s"}`)
1823
return parts.join(", ")
1924
}
2025

@@ -44,12 +49,12 @@ export function buildSummaryMarkdown(models, tools = []) {
4449
const allDiagnostics = [...models.flatMap((m) => m.diagnostics), ...tools.flatMap((t) => t.diagnostics)]
4550
const totalExamplesFailed = models.reduce((sum, m) => sum + m.examplesFailed, 0)
4651
const totalExamples = models.reduce((sum, m) => sum + m.examplesTotal, 0)
47-
const { errors, warnings } = countBySeverity(allDiagnostics)
52+
const { errors, warnings, notices } = countBySeverity(allDiagnostics)
4853

4954
lines.push("")
5055
lines.push(
5156
`**${models.length} model(s) checked** · ${totalExamples - totalExamplesFailed}/${totalExamples} example(s) converge · ` +
52-
`${errors} error(s), ${warnings} warning(s)`,
57+
`${errors} error(s), ${warnings} warning(s), ${notices} notice(s)`,
5358
)
5459
lines.push("")
5560

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// A valid model whose judgement does not hold up: nothing fires below 10000,
2+
// so the result silently stays at its initial value there, and the declared
3+
// ceiling of 20 % is never reached — the rules give at most 10 %. Neither is a
4+
// reason to reject the document, and neither may pass in silence.
5+
category "Sales"
6+
7+
object Purchase
8+
amount is money
9+
10+
utility "Calculate discount"
11+
accepts Purchase
12+
returns money
13+
starts with 0
14+
15+
rule "Large purchase"
16+
if amount is at least 10000
17+
then add 10 percent of field amount
18+
19+
property "Discount is capped"
20+
result is at most 20 percent of field amount
21+
22+
example "Large purchase"
23+
given amount equals 20000
24+
expected result equals 2000

.github/actions/fts-check/test/fixtures/valid.fts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// A model with nothing to report: the two rules partition the whole range of
2+
// "amount" between them, so no input falls through to the initial value and no
3+
// two rules ever apply at once. `check` reads the input space now, and a
4+
// fixture that stands for "fully valid" has to be clean by that measure too.
15
category "Sales"
26

37
object Purchase
@@ -9,9 +13,13 @@ category "Sales"
913
returns money
1014
starts with 0
1115

16+
rule "Regular purchase"
17+
if amount is less than 10000
18+
then result equals 0
19+
1220
rule "Large purchase"
1321
if amount is at least 10000
14-
then add 10 percent of field amount
22+
then result equals 2000
1523

1624
example "Regular purchase"
1725
given amount equals 5000

.github/actions/fts-check/test/run.test.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,39 @@ test("a valid model: zero diagnostics, exit not failed, correct counts", async (
3939
}
4040
})
4141

42+
test("a model whose judgement does not hold up: warnings that annotate but do not fail", async () => {
43+
const dir = await makeWorkspace()
44+
try {
45+
await copyFile(join(fixtures, "judgement.fts"), join(dir, "examples", "judgement.fts"))
46+
const result = await run({ workspaceDir: dir, paths: "**/*.fts" })
47+
48+
/* The document is valid and its example converges, so the run must pass.
49+
What `check` now adds is its reading of the input space, and it arrives
50+
as warnings: a hole below 10000, and a ceiling of 20 % the rules never
51+
reach. Neither may fail a build; both must be visible on the pull
52+
request, on the line that declares them, together with what to do. */
53+
assert.equal(result.failed, false)
54+
assert.equal(result.counts.errors, 0)
55+
assert.equal(result.counts.examplesFailed, 0)
56+
57+
const hole = result.annotations.find((line) => line.includes("FTS_COVERAGE_HOLE"))
58+
assert.ok(hole, "дыра в покрытии аннотирована")
59+
assert.match(hole, /^::warning file=examples\/judgement\.fts,line=10/, "на строке утилиты")
60+
61+
const unattainable = result.annotations.find((line) => line.includes("FTS_PROPERTY_UNATTAINABLE"))
62+
assert.ok(unattainable, "недостижимый предел аннотирован")
63+
assert.match(unattainable, /^::warning file=examples\/judgement\.fts,line=19/, "на строке свойства")
64+
/* `%0A` и `%25` — экранирование самой GitHub: подсказка идёт второй
65+
строкой сообщения, а процент в ней экранирован как `%25`. */
66+
assert.match(unattainable, /%0Aправила дотягивают до 10 %25 от поля/u, "подсказка доехала до аннотации")
67+
68+
const strict = await run({ workspaceDir: dir, paths: "**/*.fts", failOnWarning: true })
69+
assert.equal(strict.failed, true, "--fail-on-warning всё же ловит их")
70+
} finally {
71+
await rm(dir, { recursive: true, force: true })
72+
}
73+
})
74+
4275
test("a broken model: annotation with the right line, and a failed run", async () => {
4376
const dir = await makeWorkspace()
4477
try {

src/browser.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// Browser-safe public surface. Strict certificate production and verification
22
// stay on the trusted server boundary because they use Node.js cryptography.
3+
export * from "./coverage.js"
34
export * from "./diagnostics.js"
45
export * from "./domain.js"
56
export * from "./interpreter.js"
67
export * from "./model.js"
78
export * from "./natural-parser.js"
89
export * from "./parser.js"
10+
export * from "./spans.js"
911
export * from "./stdlib.js"
1012
export * from "./templates.js"
1113
export * from "./utility.js"

src/cli.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface CliOptions {
2323
inputFile?: string
2424
mode: VisualizationMode
2525
pretty: boolean
26+
coverage: boolean
2627
}
2728

2829
export async function main(argv = process.argv.slice(2)): Promise<number> {
@@ -56,7 +57,7 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
5657
output = compile(source)
5758
break
5859
case "check": {
59-
const result = validate(compile(source))
60+
const result = validate(compile(source), { coverage: options.coverage })
6061
output = result
6162
if (!result.valid) {
6263
writeJson(output, options.pretty, process.stderr)
@@ -126,6 +127,7 @@ function parseArgs(argv: string[]): CliOptions {
126127
let inputFile: string | undefined
127128
let mode: VisualizationMode = "all"
128129
let pretty = false
130+
let coverage = true
129131

130132
for (let index = 0; index < argv.length; index += 1) {
131133
const arg = argv[index]!
@@ -150,6 +152,8 @@ function parseArgs(argv: string[]): CliOptions {
150152
if (inputFile === undefined) throw new Error("--input requires a JSON file")
151153
} else if (arg === "--pretty") {
152154
pretty = true
155+
} else if (arg === "--no-coverage") {
156+
coverage = false
153157
} else {
154158
positional.push(arg)
155159
}
@@ -160,7 +164,9 @@ function parseArgs(argv: string[]): CliOptions {
160164
file: positional[1] ?? "-",
161165
mode,
162166
pretty,
167+
coverage,
163168
}
169+
if (!coverage && result.command !== "check") throw new Error("--no-coverage is available only for check")
164170
if (outDir !== undefined && result.command !== "generate") throw new Error("--out is available only for generate")
165171
if (utilityName !== undefined && result.command !== "run") throw new Error("--utility is available only for run")
166172
if (inputFile !== undefined && result.command !== "run") throw new Error("--input is available only for run")
@@ -215,7 +221,7 @@ const helpText = `FTS — Formal Type Surface
215221
216222
Usage:
217223
fts compile [file|-] [--pretty]
218-
fts check [file|-] [--pretty]
224+
fts check [file|-] [--pretty] [--no-coverage]
219225
fts prove [file|-] [--context context.json] [--pretty]
220226
fts certify [file|-] [--context context.json] [--pretty]
221227
fts verify [file|-] --context context.json --certificate proof.json [--pretty]
@@ -228,6 +234,10 @@ Usage:
228234
fts version
229235
230236
Commands emit JSON to stdout. Diagnostics are JSON on stderr and failures use a non-zero exit code.
237+
238+
check also reads the input space of every utility: holes in rule coverage, overlaps whose
239+
outcome depends on declaration order, property limits no input ever reaches. Those are
240+
warnings — they never make a document invalid. --no-coverage turns the analysis off.
231241
`
232242

233243
if (invokedDirectly(import.meta.url)) {

0 commit comments

Comments
 (0)