dsh-logicprobe 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.en-US.md +79 -14
  2. package/README.md +79 -14
  3. package/lib/baseline.js +142 -0
  4. package/lib/client.js +56 -1
  5. package/lib/compose-tool.js +1 -1
  6. package/lib/concurrency.js +14 -0
  7. package/lib/data-engine.js +42 -8
  8. package/lib/engine.js +379 -79
  9. package/lib/export-tool.js +27 -2
  10. package/lib/granularity.js +237 -0
  11. package/lib/index.js +63 -41
  12. package/lib/report-diff-tool.js +39 -0
  13. package/lib/structure-tool.js +64 -0
  14. package/lib/structure.js +648 -0
  15. package/lib/tool.js +3 -3
  16. package/lib/types/baseline.d.ts +69 -0
  17. package/lib/types/concurrency.d.ts +10 -0
  18. package/lib/types/data-engine.d.ts +13 -1
  19. package/lib/types/engine.d.ts +138 -1
  20. package/lib/types/granularity.d.ts +106 -0
  21. package/lib/types/index.d.ts +9 -8
  22. package/lib/types/report-diff-tool.d.ts +8 -0
  23. package/lib/types/structure-tool.d.ts +10 -0
  24. package/lib/types/structure.d.ts +156 -0
  25. package/lib/types/tool.d.ts +2 -2
  26. package/lib/types/uml.d.ts +89 -0
  27. package/lib/uml-tool.js +36 -7
  28. package/lib/uml.js +217 -23
  29. package/locale/en.json +6 -0
  30. package/locale/zh.json +6 -0
  31. package/package.json +16 -14
  32. package/skills/logicprobe/SKILL.md +84 -58
  33. package/skills/logicprobe/references/dsh-model-schema.md +81 -10
  34. package/skills/logicprobe/references/gap-routing-guide.md +37 -36
  35. package/skills/logicprobe/references/hash-spec.md +128 -0
  36. package/skills/logicprobe/references/logic-verification-guide.md +27 -250
  37. package/skills/logicprobe-concurrency/SKILL.md +43 -0
  38. package/skills/{logicprobe → logicprobe-concurrency}/references/concurrency-risk-guide.md +1 -1
  39. package/skills/logicprobe-datamodel/SKILL.md +5 -4
  40. package/skills/logicprobe-datamodel/references/data-model-guide.md +1 -1
  41. package/skills/logicprobe-datamodel/references/data-model-schema.md +1 -1
  42. package/skills/logicprobe-structure/SKILL.md +95 -0
  43. package/skills/logicprobe-structure/references/structure-review-guide.md +177 -0
  44. package/skills/logicprobe-uml/SKILL.md +55 -0
  45. package/skills/{logicprobe → logicprobe-uml}/references/uml-modeling-guide.md +120 -8
  46. package/src/baseline.ts +180 -0
  47. package/src/client.js +56 -1
  48. package/src/compose-tool.ts +47 -47
  49. package/src/concurrency.ts +23 -0
  50. package/src/data-engine.ts +51 -9
  51. package/src/engine.ts +441 -67
  52. package/src/export-tool.ts +73 -48
  53. package/src/granularity.ts +312 -0
  54. package/src/index.ts +63 -41
  55. package/src/report-diff-tool.ts +43 -0
  56. package/src/structure-tool.ts +68 -0
  57. package/src/structure.ts +724 -0
  58. package/src/tool.ts +3 -3
  59. package/src/uml-tool.ts +38 -8
  60. package/src/uml.ts +275 -24
  61. package/skills/logicprobe/references/logicprobe-engine.py +0 -4943
  62. package/skills/logicprobe/references/verification-harness.py +0 -959
  63. package/skills/logicprobe-datamodel/references/data-model-harness.py +0 -528
@@ -1,47 +1,47 @@
1
- import { defineTool } from '@deepseek-ai/dsh-tools'
2
- import type { JsonValue } from './json-value.js'
3
- import { runCompositionVerification } from './engine.js'
4
-
5
- export const LOGICPROBE_COMPOSE_TOOL_NAME = 'logicprobe_compose_verify'
6
-
7
- /**
8
- * DSH tool wrapping multi-machine composition verification:
9
- * product-space reachability with rendezvous handshakes (C1 deadlock, C2 sync).
10
- */
11
- export const logicProbeComposeTool = defineTool({
12
- name: LOGICPROBE_COMPOSE_TOOL_NAME,
13
- description:
14
- 'Run composition verification over two or more LogicModelV1 state machines (logicprobe). Pass machines as an array of models and optionally rendezvous: a list of handshake events that must fire simultaneously across all machines declaring them (at least two participants, all jointly enabled, guards held; a terminal machine stops participating). Non-rendezvous events advance exactly one firing machine. Returns C1 composition-deadlock findings (a reachable state where no machine can advance while at least one is not terminal) and C2 rendezvous-never-fires findings. Each machine validates against the same schema as logicprobe_verify.',
15
- parameters: {
16
- machines: {
17
- type: 'json',
18
- required: true,
19
- description: 'Array of LogicModelV1 state-machine models to compose (two or more).',
20
- },
21
- rendezvous: {
22
- type: 'json',
23
- description: 'Optional array of handshake event names shared by the machines.',
24
- },
25
- maxStates: {
26
- type: 'integer',
27
- description: 'Maximum composite states to explore. Default 10000.',
28
- },
29
- },
30
- output: {
31
- schema: {
32
- type: 'json',
33
- description: 'Composition verification report with C1/C2 findings.',
34
- },
35
- render(_args, value) {
36
- return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }]
37
- },
38
- },
39
- timeoutMs: 10_000,
40
- isConcurrencySafe: () => true,
41
- async execute(args) {
42
- return runCompositionVerification(args.machines as unknown[], {
43
- rendezvous: args.rendezvous as string[] | undefined,
44
- maxStates: args.maxStates,
45
- }) as unknown as JsonValue
46
- },
47
- })
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import type { JsonValue } from './json-value.js'
3
+ import { runCompositionVerification } from './engine.js'
4
+
5
+ export const LOGICPROBE_COMPOSE_TOOL_NAME = 'logicprobe_compose_verify'
6
+
7
+ /**
8
+ * DSH tool wrapping multi-machine composition verification:
9
+ * product-space reachability with rendezvous handshakes (C1 deadlock, C2 sync).
10
+ */
11
+ export const logicProbeComposeTool = defineTool({
12
+ name: LOGICPROBE_COMPOSE_TOOL_NAME,
13
+ description:
14
+ 'Check two or more LogicModelV1 machines together. Pass the models in `machines`. Pass `rendezvous` for handshake events: these fire only when every declaring machine is enabled at once, and a terminal machine stops taking part. Other events advance one machine, so a machine waiting on a handshake can take any number of its own steps first. There is no rate ratio and no fairness bound. Returns C1 deadlocks (no machine can advance, and one is not terminal) and C2 handshakes that never fire. Both name the reason per machine, and C1 gives the shortest counterexample. `maxStates` caps the search; a capped run says so. Read `verdict` and `verdictReason`.',
15
+ parameters: {
16
+ machines: {
17
+ type: 'json',
18
+ required: true,
19
+ description: 'Array of LogicModelV1 state-machine models to compose (two or more).',
20
+ },
21
+ rendezvous: {
22
+ type: 'json',
23
+ description: 'Optional array of handshake event names shared by the machines.',
24
+ },
25
+ maxStates: {
26
+ type: 'integer',
27
+ description: 'Maximum composite states to explore. Default 10000.',
28
+ },
29
+ },
30
+ output: {
31
+ schema: {
32
+ type: 'json',
33
+ description: 'Composition verification report with C1/C2 findings.',
34
+ },
35
+ render(_args, value) {
36
+ return [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }]
37
+ },
38
+ },
39
+ timeoutMs: 10_000,
40
+ isConcurrencySafe: () => true,
41
+ async execute(args) {
42
+ return runCompositionVerification(args.machines as unknown[], {
43
+ rendezvous: args.rendezvous as string[] | undefined,
44
+ maxStates: args.maxStates,
45
+ }) as unknown as JsonValue
46
+ },
47
+ })
@@ -1,3 +1,4 @@
1
+ import { REPORT_SCHEMAS, verdictOfFindings, type ReportSchema, type Verdict } from './engine.js'
1
2
  export interface ConcurrencyFinding {
2
3
  code: 'CONCURRENCY_KEYWORD' | 'CONCURRENCY_ABSOLUTE_CLAIM'
3
4
  severity: 'warning' | 'error'
@@ -11,6 +12,11 @@ export interface ConcurrencyFinding {
11
12
 
12
13
  export interface ConcurrencyScanReport {
13
14
  ok: boolean
15
+ ran: boolean
16
+ verdict: Verdict
17
+ verdictReason: string
18
+ /** The versioned report contract this result follows. */
19
+ schema: ReportSchema
14
20
  findings: ConcurrencyFinding[]
15
21
  summary: {
16
22
  lines: number
@@ -19,6 +25,10 @@ export interface ConcurrencyScanReport {
19
25
  warnings: number
20
26
  errors: number
21
27
  }
28
+ /** This scan hashes no model; the field is present so every report has the same shape. */
29
+ hashes: Record<string, string>
30
+ /** What to do next, derived from the findings — never empty. */
31
+ nextSteps: string[]
22
32
  }
23
33
 
24
34
  interface KeywordRule {
@@ -100,8 +110,19 @@ export function runConcurrencyScan(text: string): ConcurrencyScanReport {
100
110
  const errors = findings.filter((finding) => finding.severity === 'error').length
101
111
  const warnings = findings.filter((finding) => finding.severity === 'warning').length
102
112
  const absoluteClaims = findings.filter((finding) => finding.code === 'CONCURRENCY_ABSOLUTE_CLAIM').length
113
+ const nextSteps: string[] = []
114
+ if (absoluteClaims > 0) {
115
+ nextSteps.push('Every absolute claim must either cite dedicated evidence (a TSan/Helgrind run, a CBMC harness, a TLA+ model of the interleaving) or be restated as a bounded, testable property.')
116
+ }
117
+ if (findings.length > absoluteClaims) {
118
+ nextSteps.push('Review each flagged keyword: a plan that names shared state, a mutex or an interrupt without a synchronization argument is incomplete, not merely verbose.')
119
+ }
120
+ nextSteps.push('This scan mines claims; it does not prove concurrency safety — route each one to the tool named in its suggestions.')
103
121
  return {
104
122
  ok: true,
123
+ ran: true,
124
+ ...verdictOfFindings(findings),
125
+ schema: REPORT_SCHEMAS.concurrency,
105
126
  findings,
106
127
  summary: {
107
128
  lines: lines.length,
@@ -110,5 +131,7 @@ export function runConcurrencyScan(text: string): ConcurrencyScanReport {
110
131
  warnings,
111
132
  errors,
112
133
  },
134
+ hashes: {},
135
+ nextSteps,
113
136
  }
114
137
  }
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto'
2
- import type { CheckResult, Finding } from './engine.js'
2
+ import { REPORT_SCHEMAS, verdictOfFindings, type CheckResult, type Finding, type ReportSchema, type Verdict } from './engine.js'
3
3
 
4
4
  export const DATA_ENGINE_SCHEMA_VERSION = 1
5
5
 
@@ -129,9 +129,17 @@ export interface DataComparisonSummary {
129
129
  }
130
130
 
131
131
  export interface DataVerificationReport {
132
+ /** The engine ran and the input was well-formed enough to produce this report. Read `verdict` for the review outcome. */
132
133
  ok: boolean
134
+ ran: boolean
135
+ verdict: Verdict
136
+ verdictReason: string
137
+ /** The versioned report contract this result follows. */
138
+ schema: ReportSchema
133
139
  schemaVersion: 1
134
140
  modelHash: string
141
+ /** Every hash this report carries, in one place. The data-model hash follows the same normalization as the published spec, but is not covered by it. */
142
+ hashes: { modelHash: string }
135
143
  summary: {
136
144
  entities: number
137
145
  fields: number
@@ -142,6 +150,8 @@ export interface DataVerificationReport {
142
150
  }
143
151
  checks: CheckResult[]
144
152
  comparison?: DataComparisonSummary
153
+ /** What to do next, derived from the findings — never empty. */
154
+ nextSteps: string[]
145
155
  }
146
156
 
147
157
  const KNOWN_TYPES = new Set<string>([
@@ -913,18 +923,25 @@ function buildDataComparisonSummary(before: DataModelV1, after: DataModelV1, opt
913
923
  export function runDataVerification(input: unknown, options: DataVerificationOptions = {}): DataVerificationReport {
914
924
  const validation = validateDataModel(input)
915
925
  if (!validation.ok) {
926
+ const findings: Finding[] = validation.errors.map((message) => ({ code: 'DATA_MODEL_INVALID', severity: 'error', message }))
927
+ const modelCheck: CheckResult = {
928
+ id: 'DATA_MODEL',
929
+ name: 'Data Model Validation',
930
+ status: 'fail',
931
+ detail: 'Data model schema validation failed: ' + validation.errors.length + ' errors',
932
+ findings,
933
+ }
916
934
  return {
917
935
  ok: false,
936
+ ran: true,
937
+ ...verdictOfFindings(findings),
938
+ schema: REPORT_SCHEMAS.datamodel,
918
939
  schemaVersion: 1,
919
940
  modelHash: '',
941
+ hashes: { modelHash: '' },
920
942
  summary: { entities: 0, fields: 0, errors: validation.errors.length, warnings: 0, checksRun: 0 },
921
- checks: [{
922
- id: 'DATA_MODEL',
923
- name: 'Data Model Validation',
924
- status: 'fail',
925
- detail: 'Data model schema validation failed: ' + validation.errors.length + ' errors',
926
- findings: validation.errors.map((message) => ({ code: 'DATA_MODEL_INVALID', severity: 'error', message })),
927
- }],
943
+ checks: [modelCheck],
944
+ nextSteps: dataNextSteps([modelCheck]),
928
945
  }
929
946
  }
930
947
  const model = validation.model
@@ -950,10 +967,15 @@ export function runDataVerification(input: unknown, options: DataVerificationOpt
950
967
  if (options.beforeModel !== undefined) {
951
968
  const beforeValidation = validateDataModel(options.beforeModel)
952
969
  if (!beforeValidation.ok) {
970
+ const beforeFindings: Finding[] = beforeValidation.errors.map((message) => ({ code: 'BEFORE_DATA_MODEL_INVALID', severity: 'error', message }))
953
971
  return {
954
972
  ok: false,
973
+ ran: true,
974
+ ...verdictOfFindings([...checks.flatMap((check) => check.findings), ...beforeFindings]),
975
+ schema: REPORT_SCHEMAS.datamodel,
955
976
  schemaVersion: 1,
956
977
  modelHash: dataModelHash(model),
978
+ hashes: { modelHash: dataModelHash(model) },
957
979
  summary: {
958
980
  entities: model.entities.length,
959
981
  fields: allFieldPaths(model).length,
@@ -968,9 +990,10 @@ export function runDataVerification(input: unknown, options: DataVerificationOpt
968
990
  name: 'Before Data Model Validation',
969
991
  status: 'fail',
970
992
  detail: 'Before data model schema validation failed: ' + beforeValidation.errors.length + ' errors',
971
- findings: beforeValidation.errors.map((message) => ({ code: 'BEFORE_DATA_MODEL_INVALID', severity: 'error', message })),
993
+ findings: beforeFindings,
972
994
  },
973
995
  ],
996
+ nextSteps: dataNextSteps(checks),
974
997
  }
975
998
  }
976
999
  const before = beforeValidation.model
@@ -986,8 +1009,12 @@ export function runDataVerification(input: unknown, options: DataVerificationOpt
986
1009
  const warnings = checks.reduce((sum, check) => sum + check.findings.filter((finding) => finding.severity === 'warning').length, 0)
987
1010
  return {
988
1011
  ok: true,
1012
+ ran: true,
1013
+ ...verdictOfFindings(checks.flatMap((check) => check.findings)),
1014
+ schema: REPORT_SCHEMAS.datamodel,
989
1015
  schemaVersion: 1,
990
1016
  modelHash: dataModelHash(model),
1017
+ hashes: { modelHash: dataModelHash(model) },
991
1018
  summary: {
992
1019
  entities: model.entities.length,
993
1020
  fields: allFieldPaths(model).length,
@@ -996,6 +1023,21 @@ export function runDataVerification(input: unknown, options: DataVerificationOpt
996
1023
  checksRun: checks.length,
997
1024
  },
998
1025
  checks,
1026
+ nextSteps: dataNextSteps(checks),
999
1027
  ...(comparison === undefined ? {} : { comparison }),
1000
1028
  }
1001
1029
  }
1030
+
1031
+ /**
1032
+ * The next-steps list every data report carries, derived only from the checks so two
1033
+ * runs over the same model produce the same list.
1034
+ */
1035
+ function dataNextSteps(checks: CheckResult[]): string[] {
1036
+ const steps: string[] = []
1037
+ const failed = checks.filter((check) => check.status === 'fail').map((check) => check.id + ' ' + check.name)
1038
+ const warningCodes = [...new Set(checks.flatMap((check) => check.findings).filter((finding) => finding.severity === 'warning').map((finding) => finding.code))].sort()
1039
+ if (failed.length > 0) steps.push('Resolve the failing checks first: ' + failed.join(', ') + '.')
1040
+ if (warningCodes.length > 0) steps.push('Review the warning findings (' + warningCodes.join(', ') + '): a nullable field or a missing index is a decision, not silence.')
1041
+ steps.push('Re-run with beforeModel plus the mapping/copy/migration options after the change to prove the data behaviour did not regress (DD1-DD4).')
1042
+ return steps
1043
+ }